Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ jobs:
- name: Check native macOS picker metadata and profile transactions
run: go test ./internal/autocomplete -run '^TestPicker' -count=1 -v
- name: Check native macOS shell targeting and setup decisions
run: go test ./pkg/custom -run '^TestImagePicker(Shell|FirstRun|TabChoice|MacOS)' -count=1 -v
run: go test ./internal/commands/images ./pkg/custom -run '^TestImagePicker(Shell|FirstRun|TabChoice|MacOS)' -count=1 -v

minimum-unix-shells:
name: Minimum Unix executable suggestions
Expand Down Expand Up @@ -833,7 +833,7 @@ jobs:
run: go test ./internal/autocomplete -run '^TestPicker(ProfileRecovery.*|Rename.*|RecoveryDirectory.*|Windows.*|FileMetadataPreservesZoneIdentifierAtSnapshotRecheck|ReplacementMetadataKeepsSameInheritedPermissions|SecurityDescriptorComparisonPreservesPermissions|InstallRoundTrip|InstallConfigDirectoryChanges|InstallIgnoresUnusedConfigDirectory|InstallConfigDirectoryRefusalPreservesFiles|InstallPreservesHardLinkedProfiles|ProfileHardLinkAddedAfterSnapshot|ScriptContentChecksLegacyNamespaces|InstallRetriesSupersededScriptCleanup|RemoveRetriesScriptCleanup|RemoveMissingProfileParentCleansOwnedScripts|RemoveMissingProfileAndScriptDirectoriesNoOp|RemoveMissingProfileParentReappearsWhileWaiting|RemovePreservesPersonalLineBoundaries|InstalledDetectsAndRefreshesOlderScript)$' -count=1 -v
- name: Check Windows shell home targeting
if: matrix.existing-checks
run: go test ./pkg/custom -run '^TestImagePicker(WindowsShellHome|ShellConsent)' -count=1 -v
run: go test ./internal/commands/images -run '^TestImagePicker(WindowsShellHome|ShellConsent)' -count=1 -v
- name: Check native PowerShell executable suggestions
shell: pwsh
run: |
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/help-compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ jobs:
set -euo pipefail
go test -p 2 ./internal/imageprefs -count=1 -timeout=3m -v -json -run '^(TestImagePreferenceDefaultAndReplacement|TestImagePreferenceInvalidSettingsRemainUntouched|TestImagePreferenceConcurrentWritesAndCancellation|TestImagePreferenceWaitingWriterKeepsNewSchema|TestImagePreferenceWaitingWriterCancellationAndReplacement|TestImagePreferenceRejectsUnsafePaths|TestImagePreferenceRejectsUnsafeWriterLocks|TestImagePreferencePermissionFailuresKeepFile)$' \
| tee "$RUNNER_TEMP/image-preference-state-tests.jsonl"
go test -p 2 ./pkg/custom -count=1 -timeout=3m -v -json -run '^(TestImagePickerStateRoundTripAndPrivateReplacement|TestImagePickerStateIgnoresLegacyPrompt|TestImagePickerStateMalformedAndUnknownRemainUntouched|TestImagePickerStateCanceledWriteKeepsPreviousAndCleansTemporary|TestImagePickerStateConcurrentSavesKeepWholeRecords|TestImagePickerStateLimitIncludesPromptWithoutLimitingGeneration|TestImagePickerStateAcceptsExactPersistenceLimit|TestImagePickerStateRejectsUnstableSaveFolders|TestImagePickerStateRejectsSpecialFilesAndSymlinks|TestImagePickerStateRejectsExposedPermissions|TestImagePickerStateWriteFailureKeepsPrevious)$' \
go test -p 2 ./internal/commands/images -count=1 -timeout=3m -v -json -run '^(TestImagePickerStateRoundTripAndPrivateReplacement|TestImagePickerStateIgnoresLegacyPrompt|TestImagePickerStateMalformedAndUnknownRemainUntouched|TestImagePickerStateCanceledWriteKeepsPreviousAndCleansTemporary|TestImagePickerStateConcurrentSavesKeepWholeRecords|TestImagePickerStateLimitIncludesPromptWithoutLimitingGeneration|TestImagePickerStateAcceptsExactPersistenceLimit|TestImagePickerStateRejectsUnstableSaveFolders|TestImagePickerStateRejectsSpecialFilesAndSymlinks|TestImagePickerStateRejectsExposedPermissions|TestImagePickerStateWriteFailureKeepsPrevious)$' \
| tee "$RUNNER_TEMP/image-picker-state-tests.jsonl"
- name: Test Windows image preferences and picker state
if: matrix.image-state && runner.os == 'Windows' && matrix.shard != 'dispatch'
Expand All @@ -546,7 +546,7 @@ jobs:
go test -p 2 ./internal/imageprefs -count=1 -timeout=3m -v -json -run '^(TestImagePreferenceDefaultAndReplacement|TestImagePreferenceInvalidSettingsRemainUntouched|TestImagePreferenceConcurrentWritesAndCancellation|TestImagePreferenceWaitingWriterKeepsNewSchema|TestImagePreferenceWaitingWriterCancellationAndReplacement|TestImagePreferenceRejectsUnsafePaths|TestImagePreferenceRejectsUnsafeWriterLocks|TestImagePreferenceWindowsAccessDeniedKeepsFile)$' |
Tee-Object -FilePath (Join-Path $env:RUNNER_TEMP 'image-preference-state-tests.jsonl') -Encoding utf8NoBOM -ErrorAction Stop
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go test -p 2 ./pkg/custom -count=1 -timeout=3m -v -json -run '^(TestImagePickerStateRoundTripAndPrivateReplacement|TestImagePickerStateIgnoresLegacyPrompt|TestImagePickerStateMalformedAndUnknownRemainUntouched|TestImagePickerStateCanceledWriteKeepsPreviousAndCleansTemporary|TestImagePickerStateConcurrentSavesKeepWholeRecords|TestImagePickerStateLimitIncludesPromptWithoutLimitingGeneration|TestImagePickerStateAcceptsExactPersistenceLimit|TestImagePickerStateRejectsUnstableSaveFolders|TestImagePickerStateRejectsSpecialFilesAndSymlinks|TestImagePickerStateWindowsAccessDeniedKeepsPrevious)$' |
go test -p 2 ./internal/commands/images -count=1 -timeout=3m -v -json -run '^(TestImagePickerStateRoundTripAndPrivateReplacement|TestImagePickerStateIgnoresLegacyPrompt|TestImagePickerStateMalformedAndUnknownRemainUntouched|TestImagePickerStateCanceledWriteKeepsPreviousAndCleansTemporary|TestImagePickerStateConcurrentSavesKeepWholeRecords|TestImagePickerStateLimitIncludesPromptWithoutLimitingGeneration|TestImagePickerStateAcceptsExactPersistenceLimit|TestImagePickerStateRejectsUnstableSaveFolders|TestImagePickerStateRejectsSpecialFilesAndSymlinks|TestImagePickerStateWindowsAccessDeniedKeepsPrevious)$' |
Tee-Object -FilePath (Join-Path $env:RUNNER_TEMP 'image-picker-state-tests.jsonl') -Encoding utf8NoBOM -ErrorAction Stop
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Test request configuration
Expand Down Expand Up @@ -871,7 +871,7 @@ jobs:
picker_cases["TestImagePickerStateWriteFailureKeepsPrevious"] = []
for filename, package, cases in [
("image-preference-state-tests.jsonl", "internal/imageprefs", preference_cases),
("image-picker-state-tests.jsonl", "pkg/custom", picker_cases),
("image-picker-state-tests.jsonl", "internal/commands/images", picker_cases),
]:
suites[filename] = set().union(*(required_cases(root, children) for root, children in cases.items()))
strict_packages[filename] = "github.com/openai/openai-cli/" + package
Expand Down Expand Up @@ -1720,12 +1720,14 @@ jobs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go test -c -o (Join-Path $env:RUNNER_TEMP 'wsl-app.test') ./internal/app
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go test -c -o (Join-Path $env:RUNNER_TEMP 'wsl-images.test') ./internal/commands/images
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
go mod verify
exit $LASTEXITCODE
- name: Run compatibility checks in disposable Ubuntu WSL 1
shell: pwsh
timeout-minutes: 16
run: python -I -B scripts/check-wsl-pager.py --binary (Join-Path $env:RUNNER_TEMP 'wsl-pager.test') --process-binary (Join-Path $env:RUNNER_TEMP 'wsl-process.test') --app-binary (Join-Path $env:RUNNER_TEMP 'wsl-app.test') ${{ matrix.arguments }}
run: python -I -B scripts/check-wsl-pager.py --binary (Join-Path $env:RUNNER_TEMP 'wsl-pager.test') --process-binary (Join-Path $env:RUNNER_TEMP 'wsl-process.test') --app-binary (Join-Path $env:RUNNER_TEMP 'wsl-app.test') --images-binary (Join-Path $env:RUNNER_TEMP 'wsl-images.test') ${{ matrix.arguments }}
- name: Reconcile the owned WSL distribution
if: always()
shell: pwsh
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ it. Fix recurring generated defects in the authoritative schema or generator
when available instead of introducing broad manual changes.

Handwritten CLI features are moving into `internal/commands/<feature>/` (Codex,
tokenizer, and manpages are there now); remaining behavior lives in `pkg/custom/`. API response selection
tokenizer, manpages, files, batches, models, admin, audio, and images are there now); remaining behavior lives in `pkg/custom/`. API response selection
and reshaping live in `internal/output/transform/`. Use cohesive `internal/` helpers for
shared implementation, including text rendering and terminal escaping.
`internal/commands/` owns command-tree routing, compatibility aliases, shortcuts,
Expand Down
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ below describes the current layout as the migration proceeds.
the encoding engine.
- `internal/commands/manpages/` owns manual rendering, text/gzip file cleanup,
and tests. Application assembly supplies the shared save-receipt policy.
- `internal/commands/images/` owns image model discovery, picker drafts, saving,
streaming previews, local preview preferences, and shell setup mechanics.
Assembly supplies request/presentation policy and foreground first-run admission.
- `internal/commands/audio/` owns speech controls, local history/playback commands,
voice creation and validation, and transcript/speech response handling.
Assembly supplies request and presentation policies.
Expand Down Expand Up @@ -110,7 +113,7 @@ recurring generated problems in the authoritative schema or generator when
available.

Handwritten features are moving into `internal/commands/<feature>/`; Codex,
tokenizer, manpages, files, batches, models, admin, and audio are there now. Remaining behavior belongs in `pkg/custom/`,
tokenizer, manpages, files, batches, models, admin, audio, and images are there now. Remaining behavior belongs in `pkg/custom/`,
`internal/output/transform/`, and focused `internal/` helpers. Keep
`cmd/openai/` focused on startup and wiring. Generated code calls handwritten
request/output helpers through a generated runtime bridge.
Expand Down
13 changes: 13 additions & 0 deletions docs/architecture/repository-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,19 @@ handling. Shared choice labels live in `internal/readable/`. Native Windows
publication and voice validation contracts retain their exact test/subcase
inventories and now verify their owning packages.

Image model discovery, picker/editor state, image request preparation, saving,
stream handling, local previews/preferences, and shell setup mechanics live in
`internal/commands/images/`. Assembly supplies request preparation and presentation
callbacks, retains output-route eligibility, and owns first-run worker admission
and the foreground timeout. Feature metadata and saved-image stream cleanup stay
private to the feature. Cross-layer tests use real assembly callbacks; native
state-persistence checks retain their exact test and subcase inventories.

Delayed loading feedback shared by images and model listing lives in
`internal/output/`. Terminal writer classification lives in `internal/terminal/`
and preserves both file and borrowed-file handling. These shared packages do not
import command features or assembly.

## Product and compatibility contract

This repository ships the `openai` executable. Its Go packages are implementation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"context"
Expand All @@ -12,6 +12,7 @@ import (
"github.com/charmbracelet/x/term"
clioutput "github.com/openai/openai-cli/internal/output"
"github.com/openai/openai-cli/internal/readable"
cliterminal "github.com/openai/openai-cli/internal/terminal"
"github.com/openai/openai-cli/internal/terminalimage"
"github.com/tidwall/gjson"
)
Expand Down Expand Up @@ -50,7 +51,7 @@ func (p *imageOutputPlan) displayImageProgress(ctx context.Context, event gjson.
// Share the saved-image sizing calculation, but not its recovery messages:
// the final image has not arrived yet and there is no output file to retry.
file, ok := out.(*os.File)
if !ok || !isTerminal(out) {
if !ok || !cliterminal.IsWriterTerminal(out) {
return false, errors.New("inline previews require a terminal")
}
width, height, sizeErr := term.GetSize(file.Fd())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"bytes"
Expand Down Expand Up @@ -157,7 +157,7 @@ func TestImageProgressTerminal(t *testing.T) {
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
stream := &progressTestStream{savedImageTestStream: savedImageTestStream{events: []outputJSON{{first}, {first}, {second}, {progressTestEvent(t, "image_generation.completed", 0)}}}}
stream := &progressTestStream{savedImageTestStream: savedImageTestStream{events: []outputJSON{{Result: first}, {Result: first}, {Result: second}, {Result: progressTestEvent(t, "image_generation.completed", 0)}}}}
stream.beforeNext = func(index int) {
if index == 3 {
// TERM_SESSION_ID is empty, so opted-in font attempts stop before
Expand All @@ -176,7 +176,7 @@ func TestImageProgressTerminal(t *testing.T) {
stream.err = errors.New("synthetic-private-stream-error")
}
fmt.Fprintln(os.Stdout, "PROGRESS-CASE", scenario)
err := saveFinalImageStream(ctx, stream, plan, os.Stdout)
err := saveFinalImageStream(ctx, stream, plan, os.Stdout, false)
fmt.Fprintln(os.Stdout, "PROGRESS-END", scenario)
if scenario == "cancel" || scenario == "malformed-cancel" {
require.ErrorIs(t, err, context.Canceled)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"context"
Expand All @@ -15,6 +15,7 @@ import (
"github.com/openai/openai-cli/internal/imageoutput"
clioutput "github.com/openai/openai-cli/internal/output"
"github.com/openai/openai-cli/internal/readable"
cliterminal "github.com/openai/openai-cli/internal/terminal"
"github.com/openai/openai-cli/internal/terminalimage"
)

Expand Down Expand Up @@ -44,7 +45,7 @@ func displaySavedImages(ctx context.Context, saved []imageoutput.SavedImage, out
// displaySavedImage previews only bytes from the completed save operation. It
// reports completed rendering and never opens a viewer or changes the saved file.
func displaySavedImage(ctx context.Context, saved imageoutput.SavedImage, out, diagnostics io.Writer, mode string) (bool, error) {
protocol := savedImageProtocol(mode, isTerminal(out), os.Getenv)
protocol := savedImageProtocol(mode, cliterminal.IsWriterTerminal(out), os.Getenv)
if protocol == "" {
return false, nil
}
Expand Down Expand Up @@ -75,7 +76,7 @@ func displaySavedImage(ctx context.Context, saved imageoutput.SavedImage, out, d
// local preview, whose caller reports invalid input before reaching this point.
func displayDecodedSavedImage(ctx context.Context, img image.Image, out, diagnostics io.Writer, protocol string) error {
file, ok := out.(*os.File)
if !ok || !isTerminal(out) {
if !ok || !cliterminal.IsWriterTerminal(out) {
return errors.New("inline previews require a terminal")
}
if diagnostics == nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"bytes"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"bytes"
Expand Down
68 changes: 68 additions & 0 deletions internal/commands/images/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package images

import (
"context"
"io"

"github.com/openai/openai-cli/internal/imagemodels"
clioutput "github.com/openai/openai-cli/internal/output"
"github.com/urfave/cli/v3"
)

// Test-only access to private feature state for direct integration tests.
type TestLoadingTestWriter = loadingTestWriter

var TestIsTerminal = isTerminal
var TestImagePickerTabChoicePath = imagePickerTabChoicePath
var TestImagePickerSelectShellTargets = imagePickerSelectShellTargets
var TestRegisterImageSavingFlags = registerImageSavingFlags
var TestFirstRunFileSnapshot = firstRunFileSnapshot
var TestImageInlinePreferenceCommands = imageInlinePreferenceCommands
var TestImageSavingWorkflow = imageSavingWorkflow
var TestSetupImagePickerFirstRun = setupImagePickerFirstRun
var TestImageModelTestRow = imageModelTestRow
var TestImageInlinePreferenceLocation = imageInlinePreferenceLocation
var TestImagePickerShellTarget = imagePickerShellTarget

type TestImageModelsReport = imageModelsReport

var TestPickerShellSetupHome = pickerShellSetupHome
var TestWriteImageModelsData = writeImageModelsData
var TestImagePickerTabDeclined = imagePickerTabDeclined
var TestImageInlinePreferencePath = imageInlinePreferencePath
var TestLegacyImagePickerTabChoicePath = legacyImagePickerTabChoicePath
var TestDeclineImagePickerTab = declineImagePickerTab
var TestFinishInteractiveImagePreview = finishInteractiveImagePreview
var TestPrivateImagePickerState = privateImagePickerState

type TestOutputJSON = outputJSON

var TestImagePickerStatePath = imagePickerStatePath

func NewOutputPlanForTest(directory, name, inline string) *imageOutputPlan {
return &imageOutputPlan{directory: directory, name: name, inline: inline}
}
func (p *imageOutputPlan) DirectoryForTest() string { return p.directory }
func (p *imageOutputPlan) SaveForTest(ctx context.Context, data []byte, out io.Writer) error {
return p.save(ctx, data, out)
}
func (p *imageOutputPlan) SetLoadingStageForTest(stage func(clioutput.LoadingStage)) {
p.loadingStage = stage
}
func WithPresentationForTest(ctx context.Context, plan *imageOutputPlan, out io.Writer) context.Context {
return context.WithValue(ctx, imagePresentationKey{}, imagePresentation{plan, out})
}
func InlineForTest(ctx context.Context) string {
return ctx.Value(imagePresentationKey{}).(imagePresentation).plan.inline
}
func PreparedBodyForTest(command *cli.Command) io.ReadCloser {
return command.Metadata[imageMultipartMetadata].(*imageMultipartPreparation).body.(io.ReadCloser)
}
func InterruptForTest(err error) error { return &imageLoadingInterrupt{err} }
func InteractiveExitForTest(code int) error { return interactiveImageExit{code} }
func ModelsErrorForTest(invocation string, extra bool, results []imagemodels.Result) *ModelsError {
return &ModelsError{invocation: invocation, extraArguments: extra, results: results}
}
func ShellPathsForTest(home, config, zdotdir string) imagePickerShellPaths {
return imagePickerShellPaths{home: home, config: config, zdotdir: zdotdir}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"errors"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package custom
package images

import (
"strings"
Expand Down
Loading
Loading