@shardflux/cli 0.16.0 → 0.18.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/README.md +47 -8
  3. package/dist/agent-commands.d.ts +9 -1
  4. package/dist/agent-commands.js +65 -16
  5. package/dist/agent-models.d.ts +61 -0
  6. package/dist/agent-models.js +79 -0
  7. package/dist/agent-state.d.ts +10 -0
  8. package/dist/agent-state.js +13 -0
  9. package/dist/agent-target-commands.d.ts +6 -1
  10. package/dist/agent-target-commands.js +113 -16
  11. package/dist/agent-targets.d.ts +17 -5
  12. package/dist/agent-targets.js +27 -11
  13. package/dist/args.d.ts +5 -1
  14. package/dist/args.js +49 -9
  15. package/dist/cli-proxy-forward.d.ts +45 -0
  16. package/dist/cli-proxy-forward.js +432 -0
  17. package/dist/cli-proxy-hub.d.ts +273 -0
  18. package/dist/cli-proxy-hub.js +749 -0
  19. package/dist/cli-proxy.d.ts +325 -0
  20. package/dist/cli-proxy.js +1016 -0
  21. package/dist/codex-cloud.d.ts +2 -1
  22. package/dist/codex-cloud.js +16 -6
  23. package/dist/command-center-actions.js +16 -1
  24. package/dist/conversation-local.js +2 -2
  25. package/dist/conversation-stash.js +4 -3
  26. package/dist/conversation-terminal.d.ts +5 -0
  27. package/dist/conversation-terminal.js +45 -6
  28. package/dist/conversations.d.ts +18 -2
  29. package/dist/conversations.js +12 -5
  30. package/dist/desktop-linux.d.ts +40 -0
  31. package/dist/desktop-linux.js +186 -0
  32. package/dist/device-approvals.d.ts +12 -0
  33. package/dist/device-approvals.js +75 -0
  34. package/dist/device-helper.d.ts +3 -1
  35. package/dist/device-helper.js +5 -2
  36. package/dist/errors.d.ts +2 -0
  37. package/dist/errors.js +1 -1
  38. package/dist/git-remote-bin.js +0 -0
  39. package/dist/handlers.js +3 -1
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +3 -0
  42. package/dist/linux-helper.d.ts +12 -0
  43. package/dist/linux-helper.js +88 -0
  44. package/dist/linux-terminal.d.ts +11 -0
  45. package/dist/linux-terminal.js +42 -0
  46. package/dist/main.js +30 -6
  47. package/dist/menubar.js +44 -4
  48. package/dist/onboarding.d.ts +2 -0
  49. package/dist/onboarding.js +59 -9
  50. package/dist/t3-asar.d.ts +10 -0
  51. package/dist/t3-asar.js +91 -0
  52. package/dist/t3-launch.d.ts +37 -0
  53. package/dist/t3-launch.js +133 -0
  54. package/dist/t3-linux.d.ts +68 -0
  55. package/dist/t3-linux.js +343 -0
  56. package/dist/t3-mode.d.ts +129 -0
  57. package/dist/t3-mode.js +676 -0
  58. package/dist/t3-phone.d.ts +17 -0
  59. package/dist/t3-phone.js +15 -0
  60. package/dist/t3-provider.d.ts +5 -3
  61. package/dist/t3-provider.js +12 -49
  62. package/dist/t3-thread-script.d.ts +18 -0
  63. package/dist/t3-thread-script.js +245 -0
  64. package/dist/terminal-onboarding.d.ts +11 -0
  65. package/dist/terminal-onboarding.js +75 -0
  66. package/dist/thread-on-computer.d.ts +2 -2
  67. package/dist/thread-on-computer.js +13 -7
  68. package/dist/update.d.ts +1 -1
  69. package/dist/update.js +28 -1
  70. package/dist/worktree-twin-engine.d.ts +31 -1
  71. package/dist/worktree-twin-engine.js +253 -40
  72. package/dist/worktree-twin.d.ts +17 -0
  73. package/dist/worktree-twin.js +147 -22
  74. package/package.json +7 -19
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.0 (not yet published)
4
+
5
+ - Linux (Ubuntu 24.04, x64 and arm64): bare `shard` opens the Shardflux app in a graphical session. The optional CPU package installs without npm install scripts, adds Shardflux to the app grid, and starts at login. Headless sessions keep terminal onboarding. The systemd user helper keeps the live twin, files and conversation mirrors running; `shard device requests`, `shard device approve <id>` and `shard device deny <id>` handle local approvals.
6
+ - The Ubuntu desktop app includes the custom tray panel, launcher capture and project/model menus, all Settings panes, local device and credential approval panels, notifications with actions, file downloads and packaged-app updates. `shard device approve --always` remembers matching in-project requests; outside-folder requests remain one-time approvals.
7
+ - T3 Code on Linux: `shard` finds T3 Code installed from its .deb or AppImage (and AppImageLauncher, Gear Lever,
8
+ flatpak and snap entries), registers the Shardflux provider in it, shows it in onboarding, and opens it from its
9
+ desktop entry. `shard agents new --in t3`, `shard agents open t3` and `shard agents targets --open-in` work as on
10
+ macOS: a T3 build with ACP providers starts each thread on its Shardflux provider in a cloud VM of its own, and T3
11
+ Code 0.0.45 opens the project VM as an SSH environment. `shard agents targets --json` reports it under `t3`
12
+ (`last_used_at`: when T3 last started). The Shardflux provider is also recognized in the latest T3 nightlies on
13
+ macOS.
14
+
15
+ - A Codex project opens in T3 Code without a Claude sign-in (T3 shows Claude as signed out, and says how to sign in), and `--json` names the T3 Connect sign-in `shard` started for the phone (`t3_link.sign_in`, or `t3_link.needs_login` when this computer has no session).
16
+ - A conversation settles when its turn ends (`done` or `unresolved` right away) and its VM takes the next message.
17
+ `shard agents open`, `shard ./ --resume` and ACP reattach to that VM instead of resuming the conversation into a new
18
+ one, `shard agents computers` and `--on` offer its computer for new threads, Try locally keeps syncing across turns,
19
+ and the transcript mirror reads it once per change. A conversation has ended once its VM is removed, or it is
20
+ archived or discarded.
21
+ - The menu bar keeps running and updating your global install: running `shard` from a source checkout, a project's
22
+ `node_modules` or `npx` no longer replaces the install it records (`SHARDFLUX_RECORD_INSTALL=1` records any run).
23
+ - `--model <model>` and `--effort <level>` on `shard ./` and `shard agents new` (also with `--on` and `--in t3`)
24
+ (0.17.0+): a new Claude or Codex thread starts with that model and reasoning effort, such as `--model opus --effort
25
+ xhigh` or `--model gpt-6-astra --effort high`. The conversation keeps them across resumes and moves. `--json` adds
26
+ `model_choice: {model, effort, applied}`; a computer whose platform predates the choice starts the thread with the
27
+ agent's defaults and says so once. Unknown values are refused before anything starts.
28
+ - `shard agents targets --json` adds `models` (0.17.0+): for `claude` and `codex`, the `models` (`id`, `label`, `short`,
29
+ `description`), the `efforts` (`id`, `label`) and the agent's own `default` (`model`, `effort`; `null` where the
30
+ agent decides at start), so the menu bar and other apps show the same choices without hardcoding them.
31
+ - T3 Code mode: `shard agents new --in t3 --prompt-file <file>` (or `--prompt`) starts the first message as a thread in
32
+ T3 Code and brings T3 to the front. With T3's Shardflux provider (T3 builds with ACP providers) the thread is launched
33
+ on the running T3 and gets its own cloud VM; with T3 Code 0.0.45 it starts on the T3 server in the project VM, or with
34
+ `--on <conversation|computer>` on that computer's. `--image` attaches images to the first message; `--model` and
35
+ `--effort` take the same choices as every new thread. `--json` returns `{ target: "t3", mode, project, t3_thread_id,
36
+ conversation_id, workspace, opened: true, focused: false, model, effort }` with the model and effort T3 got. A
37
+ requested effort that T3 did not get is `effort: null` with `effort_reason`: `not_offered` (the model has no such
38
+ level) or `models_not_known` (with the Shardflux provider, before this project's first conversation on this computer
39
+ has shown its levels). Refusals have their own codes: `t3_not_installed`, `t3_not_connected`, `t3_server_unreachable`,
40
+ `t3_version_unsupported`, `t3_on_not_supported`, `t3_model_unavailable`, `t3_thread_failed`, and `t3_open_failed` for
41
+ a thread that started while T3 Code did not open; once the workspace, conversation or thread exists their `details`
42
+ name its `workspace_key`, `conversation_id` and `t3_thread_id`, so a client opens it instead of starting it again.
43
+ - `shard agents targets --json` reports T3 Code (`t3: { installed, mode, connected, version, last_used_at }`) and where
44
+ each app's new conversations open (`open_in: { claude, codex }`). `--open-in claude=t3,codex=t3` saves the choice
45
+ (Claude: terminal, claude-desktop, web or t3; Codex: codex-app or t3). Without one, T3 is the default when it is
46
+ connected (the Shardflux provider registered and this computer signed in; with T3 Code 0.0.45, this project set up
47
+ as a T3 environment from this computer) and the most recently used of the installed apps. `shard ./` and `--in auto`
48
+ open new conversations there, a conversation as itself. `--path <folder>` reports them for that project (as
49
+ `agents new --path` names it); an agent argument wins over it.
50
+ - `shard agents open t3 <conversation>` brings the conversation's T3 thread back (`t3_thread_id`) and, like
51
+ `agents open t3 <workspace>`, opens T3 with `--json` too (`opened: true`).
52
+ - JSON errors that come with a next step carry it as `error.fix`.
53
+ - T3 Code on your phone: `shard agents new --in t3` and `shard agents open t3` also add the project to the T3 app as **Shardflux · <project>**. The first time, open the link it prints and confirm the code once; every later project appears by itself. A message from the app wakes the VM.
54
+ - `shard cli-proxy import` moves CLIProxyAPI from this computer into your account: one hosted copy holds your Claude, Codex and other subscription logins, sleeps between requests and wakes on the next one. This computer and every new VM reach it at `http://127.0.0.1:8317`, so Claude Code (`ANTHROPIC_BASE_URL`), Codex (`base_url …/v1`) and T3 Code's usage view keep their settings; streaming responses and WebSockets pass through.
55
+ - The hosted copy is made from your own config: client API keys, routing, retries, model aliases and upstream keys carry over, and your management key keeps working.
56
+ - Your local CLIProxyAPI stops for good on the port its config names: a Homebrew service with `brew services stop`; after you confirm (`--yes` for scripts), a launchd agent (stopped, disabled, its plist set aside), a Docker container (`docker stop`) or a process. Only a program recognized as CLIProxyAPI is ever stopped; anything else on the port is named with the command to run.
57
+ - Your auth files move to the hub, and the local copies move to `<auth folder>.moved-to-shardflux` (0700), where no CLIProxyAPI reads them.
58
+ - Run it on a second computer to connect that one too: it adds only the accounts that are new to the hub, names them, and keeps the hub's settings. Every project of your account uses the same hosted copy. `--config <path>` names the config when it is somewhere else.
59
+ - VMs receive only the address of the hub's VM port, which serves the model routes (OpenAI, Claude, Gemini and Codex, WebSockets included) and nothing else; the hub's logins and settings are managed from the computers that set it up. A second computer's logins are matched to the hub's by account (provider plus every account, organization, workspace and project id and email the auth file names), whatever the file names, so two accounts of one email both move.
60
+ - `shard cli-proxy export` brings your logins back to this computer: the hosted copy stops first, the logins it last refreshed are written to your auth folder (0600), this computer stops forwarding, and the command that starts your local CLIProxyAPI is shown. `shard cli-proxy import` moves them back.
61
+ - `shard cli-proxy` shows the hosted copy: its accounts (provider and email), whether it is running or sleeping, and where it is reachable, in a VM too. It starts CLIProxyAPI again when the hub answers that nothing listens. `--json` for scripts.
62
+ - Revoking an API key keeps the hub reachable: when its address was created with that key, the next request through this computer, `shard cli-proxy` or an import gives the hub a new address, uses it at once and passes it to every new VM.
63
+ - On macOS the forwarder runs as the launchd agent `dev.shardflux.cli-proxy-forward` (at login, kept running, restarted when `shard` is updated). A request the hub did not receive (while it wakes or CLIProxyAPI starts again) is sent again, POST bodies included up to 32 MiB; a client that disconnects ends its request to the hub.
64
+ - For onboarding: `detectLocalCliProxy()` and `importCliProxy(ctx)` are exported.
65
+
66
+ - A finished conversation lands in your checkout as soon as its turn ends: the menu bar's helper keeps one wait per
67
+ active conversation and hears the finished turn the moment the conversation reports it (no polling every second).
68
+ Conversations that finish together for the same branch land in one go, in the order they finished.
69
+
3
70
  ## 0.16.0 (not yet published)
4
71
 
5
72
  - Cloud sessions are named for the work: `shard ./` starts the agent with the repository or folder name and the first
@@ -308,7 +375,6 @@
308
375
 
309
376
  ## 0.12.0 (not yet published)
310
377
 
311
-
312
378
  - Add `cpu-service-fresh-inputs-v1` with `fresh_env_names`: fresh owned startup hooks/children consume allowed task values while immutable CPU initialization stays credential-free. Relevant or secret inputs retain ordinary startup.
313
379
 
314
380
  - Add explicit `pasteText` clipboard replacement with an application paste chord, LF-to-Return typing repair and unchanged screenshot settling.
@@ -316,7 +382,6 @@
316
382
  Every command and option the README shows is available from the version named here. Breaking changes ship in minor
317
383
  releases and are marked **Breaking**.
318
384
 
319
-
320
385
  - Accept caller-supplied exact desktop pixel predicates without an environment gate. Completion uses the same frozen full frame encoded as PNG; pre-satisfied refusal and timeout return no success image. Omitted predicates retain 250 ms settling.
321
386
  - Preserve explicit `initialized_cpu` service declarations through template YAML build/export/edit transport, matching SDK0.17.0.
322
387
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ See the [support guide](https://github.com/shardfluxdev/community/blob/main/SUPP
26
26
 
27
27
  ## Command center (0.15.0+)
28
28
 
29
- Run `shard` in a terminal to see live conversations. Press **Enter** to open the selected one, **/** to search,
29
+ On macOS, run `shard` in a terminal to see live conversations. Press **Enter** to open the selected one, **/** to search,
30
30
  **D** to expand or collapse finished conversations, and **?** for targets, logs, fork, park and Git actions.
31
31
  Press **n** to start a conversation: describe the task, confirm a local project folder, then choose Claude or Codex.
32
32
  Both the task and project are required, and the task becomes the first user-authored message. **Esc** cancels;
@@ -50,6 +50,18 @@ Bulk transfers batch writes automatically. Active transfers retain their byte po
50
50
 
51
51
  ## Install
52
52
 
53
+ Linux (Ubuntu 24.04, x64 and arm64) and macOS are supported. On Linux, run `shard` to sign in and
54
+ connect Claude, Codex or both (0.17.0+), then run `shard ./` in your project to attach Claude Code or Codex in place. Sign-in opens your browser;
55
+ on a headless terminal, follow the displayed URL and device code. `--accept-terms` accepts the terms notice.
56
+
57
+ The Linux helper starts automatically after setup as `~/.config/systemd/user/shardflux-helper.service`
58
+ and restarts after a CLI update. It keeps the live twin, `~/Shardflux` files and conversation history in sync.
59
+ Use `shard device requests`, `shard device approve <id>` or `shard device deny <id>` for local approvals;
60
+ desktop notifications point to pending requests. On systems without a systemd user session, the helper runs
61
+ while your `shard` commands are open. `shard agents open` opens a terminal when launched outside one, using
62
+ `$TERMINAL`, `x-terminal-emulator` or `gnome-terminal`, or prints the attach command. The Mac app adds a menu
63
+ bar, launcher and Settings window.
64
+
53
65
  ```sh
54
66
  npm i -g shardflux
55
67
  shard --help
@@ -70,14 +82,15 @@ shard ./
70
82
 
71
83
  Choose Claude Code or Codex. Claude asks for one cloud sign-in per Shardflux account; Codex uses your copied login.
72
84
  Each `shard ./` starts its own conversation VM and opens your agent’s native terminal. Later turns stay in that
73
- VM; `shard ./ --resume` selects a saved project conversation. Native terminal and ACP sessions receive the conversation tools for their own conversation, so the agent can fork, ask for a review, and report to its parent (0.14.0+). You can open conversations in separate terminals at the same time; each stays in the project's local history. `shard` opens the optional command center. On macOS the menu bar starts automatically the first
85
+ VM; `shard ./ --resume` selects a saved project conversation. Native terminal and ACP sessions receive the conversation tools for their own conversation, so the agent can fork, ask for a review, and report to its parent (0.14.0+). You can open conversations in separate terminals at the same time; each stays in the project's local history. On macOS, `shard` opens the optional command center. On macOS the menu bar starts automatically the first
74
86
  time `shard` runs, with launch at login enabled; `shard menubar stop` turns it off. See [Cloud agents](https://docs.shardflux.dev/guides/cloud-agents).
75
87
 
76
88
  T3 Code uses one **Shardflux** provider; choose a Claude Code or Codex model to start that agent in the cloud.
77
89
  Model choices come from your cloud agent's session and use the same coding tools and settings as its terminal.
78
- The provider is registered automatically when T3 Code is installed. Choose **Sign in** on its card in T3's provider
79
- settings: T3 runs `shard acp --login` in its own terminal, which creates or signs in to your account (with this Mac's
80
- Codex login, or your email) and connects Claude Code and Codex, skipping every step that is already done (0.15.0+).
90
+ The provider is registered automatically when T3 Code is installed, on macOS and on Linux from its .deb or AppImage
91
+ (0.17.0+). Choose **Sign in** on its card in T3's provider settings: T3 runs `shard acp --login` in its own terminal,
92
+ which creates or signs in to your account (with this computer's Codex login, or your email) and connects Claude Code
93
+ and Codex, skipping every step that is already done (0.15.0+).
81
94
  With T3 Code 0.0.45, `shard agents new --in t3` adds the project as an SSH environment instead.
82
95
 
83
96
  Start another thread on a computer that is already set up (0.16.0+): it shares that conversation's installed
@@ -94,6 +107,32 @@ shard ./ --on fix-login --review # a read-only Claude reviewer of it
94
107
  the new thread. Codex threads are Codex Cloud threads, on the web and your phone too. `shard agents list` shows each
95
108
  thread's conversation and computer.
96
109
 
110
+ Choose the model and reasoning effort a new thread starts with (0.17.0+):
111
+
112
+ ```sh
113
+ shard ./ --model opus --effort xhigh # Claude Opus at extra-high effort
114
+ shard ./ codex --model gpt-6-astra --effort high
115
+ shard agents new --app claude --path ./ --model sonnet --prompt "Tidy the README" --json
116
+ ```
117
+
118
+ Claude takes `fable`, `opus`, `sonnet` or `haiku` and `low`, `medium`, `high`, `xhigh` or `max`; Codex takes
119
+ `gpt-6.1-sol`, `gpt-6-astra` or `gpt-6-luna` and the same levels plus `ultra`. `default`, or leaving the flag out,
120
+ keeps the agent's own choice. The conversation keeps its model and effort when it is resumed or moved, and `--json`
121
+ adds `model_choice: {model, effort, applied}`. `shard agents targets --json` lists the choices as `models`, with menu
122
+ and pill text and the agent's own default, so apps never hardcode them. `--in t3` passes them to T3 Code.
123
+
124
+ Start a thread in T3 Code from the command line (0.17.0+):
125
+
126
+ ```sh
127
+ shard agents new --path . --app claude --in t3 --prompt-file task.md
128
+ shard agents targets --open-in claude=t3,codex=t3 # shard ./ starts new conversations in T3 Code
129
+ ```
130
+
131
+ The first message becomes a T3 thread and T3 comes to the front. With the Shardflux provider the thread runs in its own
132
+ cloud VM; with T3 Code 0.0.45 it runs on the T3 server in the project's VM, or with `--on` on that computer's.
133
+ `--image` attaches images. `shard agents targets --json` shows T3 Code's mode and where each app's new conversations
134
+ open; `shard agents open t3 <conversation>` brings its thread back.
135
+
97
136
  Git fetch and push use the coding guest’s platform transport when available, preserving Git’s binary packets (0.14.0+). Older guests retain their existing transport.
98
137
 
99
138
  To try a conversation on your Mac:
@@ -104,11 +143,11 @@ shard conversations try <conversation>
104
143
 
105
144
  Your branch opens in `.shard/worktrees/<name>`, syncs both ways, and its dev servers appear at localhost.
106
145
  Ctrl-C stops local sync. Your main checkout stays available for your own work.
107
- Device tools ask for approval on your Mac before preparing the conversation’s local worktree. With CLI 0.15.0 and menu bar 0.3.0, Review shows the exact command, file direction, paths or port mapping; access outside the conversation folder only offers Allow once. Live transcripts stay in
146
+ Device tools ask for approval on your computer before preparing the conversation’s local worktree. With CLI 0.15.0 and menu bar 0.3.0, Review shows the exact command, file direction, paths or port mapping; access outside the conversation folder only offers Allow once. Live transcripts stay in
108
147
  `~/.config/shardflux/conversations/`; ended sessions enter Claude Code’s or Codex’s native history. Transcript files up to 64 MiB are supported (0.14.0+).
109
148
  From 0.14.3, transcript copying runs independently of device approvals. Unknown entries are skipped, and failed copies retry with backoff; diagnostics name the conversation and affected entry.
110
149
 
111
- Every conversation's committed files are on your Mac too (0.16.0+, with the menu bar app running):
150
+ Every conversation's committed files are on your computer too (with the helper running; macOS 0.16.0+, Linux 0.17.0+):
112
151
  `~/Shardflux/<repository>/<base branch>/<conversation>/` holds what each conversation changed or added, read-only,
113
152
  and updates when its branch moves. Large files are listed as `<file>.placeholder.txt`; download one with the menu
114
153
  bar's **Download** or:
@@ -1252,7 +1291,7 @@ connection it configured and the file it is in (0.13.3+).
1252
1291
 
1253
1292
  `shard agents fork` or `f` in the command center copies the whole running state into an independent workspace and
1254
1293
  conversation. Open in your terminal, Claude Desktop or the Codex app, and the terminal Claude conversation in
1255
- claude.ai or mobile. Session titles start with `[SF] `. Waiting agents stay reachable on their own connections;
1294
+ claude.ai or mobile. Sessions are titled with their task and repository, like `Add CSV export · todo-app`. Waiting agents stay reachable on their own connections;
1256
1295
  see the [settings and billing reference](https://docs.shardflux.dev/limits#cloud-agent-reachability).
1257
1296
  Claude needs one cloud sign-in per account; Codex uses your copied login.
1258
1297
 
@@ -1,6 +1,8 @@
1
1
  import type { CellClient, PtySession, Workspace } from '@shardflux/sdk';
2
2
  import type { Ctx, Handler } from './commands.js';
3
3
  import type { Values } from './args.js';
4
+ import type { T3Install, T3ThreadRequest } from './t3-mode.js';
5
+ import type { ModelChoice } from './agent-models.js';
4
6
  /** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
5
7
  export declare function disambiguatedSlug(slug: string, identity: string): string;
6
8
  export declare function projectTemplate(ctx: Ctx, remote: string | null): Promise<string>;
@@ -9,7 +11,10 @@ export declare function projectTemplate(ctx: Ctx, remote: string | null): Promis
9
11
  export declare function agentPty(cell: CellClient, app: 'claude' | 'codex', agent: string, approvals: boolean, term: string, signal?: AbortSignal, place?: string, setupHash?: string, cwd?: string, ptyName?: string, onLaunch?: () => void, deferSetup?: boolean, conversation?: {
10
12
  id: string;
11
13
  session: string | null;
12
- }, task?: string): Promise<PtySession>;
14
+ }, task?: string, launch?: {
15
+ choice?: ModelChoice;
16
+ help?: () => Promise<string>;
17
+ }): Promise<PtySession>;
13
18
  /** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
14
19
  export declare function sessionTask(text: string | undefined): string | undefined;
15
20
  export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex', agent: string, project: string, values: Values, setupHash?: string, cwd?: string, conversation?: {
@@ -17,6 +22,9 @@ export declare function terminal(ctx: Ctx, ws: Workspace, app: 'claude' | 'codex
17
22
  session: string | null;
18
23
  pty?: string;
19
24
  title?: string;
25
+ choice?: ModelChoice;
20
26
  }): Promise<number>;
21
27
  export declare const launchProject: Handler;
28
+ /** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
29
+ export declare function launchProjectWith(ctx: Ctx, values: Values, [path, explicitApp]: string[], thread?: T3ThreadRequest, install?: T3Install): Promise<number>;
22
30
  export declare const AGENT_HANDLERS: Record<string, Handler>;
@@ -1,3 +1,4 @@
1
+ import { linuxTerminalSession, openLinuxTerminal } from "./linux-terminal.js";
1
2
  import { repositoryName } from "./files-mirror.js";
2
3
  import { prepareSetupCache } from "./setup-cache.js";
3
4
  import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
@@ -23,10 +24,12 @@ import { progress } from "./progress.js";
23
24
  import { rememberLaunch } from "./agent-live.js";
24
25
  import { prepareAgentMachine } from "./agent-machine.js";
25
26
  import { installedTargets } from "./agent-targets.js";
27
+ import { openInFor } from "./t3-mode.js";
26
28
  import { openClaudeDesktopSession, openDesktopWorkspace, queueT3Prewarm } from "./agent-target-commands.js";
27
29
  import { submitInitialPrompt, prepareInitialCodexProject } from "./initial-prompt.js";
28
30
  import { conversationWorkspace, conversationsUnavailable, getConversation, listConversations, resumeConversation } from "./conversations.js";
29
31
  import { conversationTerminal } from "./conversation-terminal.js";
32
+ import { CHOICE_PENDING, choiceReport, hasChoice, launchChoice, modelChoice, runFlags } from "./agent-models.js";
30
33
  const active = (session) => session.state === 'running' || session.state === 'starting';
31
34
  /** `<slug>-<6 hex of sha256(identity)>`: the same repository gets the same key on every machine. */
32
35
  export function disambiguatedSlug(slug, identity) {
@@ -50,7 +53,7 @@ export async function projectTemplate(ctx, remote) {
50
53
  }
51
54
  /** `place` is the repository or folder name session titles end with ("todo-app"), `task` what a new session is titled
52
55
  * with before the guest names it from its first prompt (a first prompt line or the conversation's title). */
53
- export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task) {
56
+ export async function agentPty(cell, app, agent, approvals, term, signal, place, setupHash, cwd, ptyName, onLaunch, deferSetup = false, conversation, task, launch) {
54
57
  const id = `shard-${ptyName || agent}-${app}`;
55
58
  // Arrival and changed locks queue the same durable guest job even when the
56
59
  // native agent is already alive. Setup never becomes its first user turn.
@@ -71,18 +74,19 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
71
74
  onLaunch?.();
72
75
  let conversationArgs = [];
73
76
  if (conversation) {
74
- const help = await cell.exec.run(['sf-agent', 'run', app, '--help'], { timeoutMs: 10_000, maxOutputBytes: 8192, signal });
75
- if ((help.stdout + help.stderr).includes('-conversation'))
77
+ const help = await (launch?.help ?? runHelp(cell, app, signal))();
78
+ if (help.includes('-conversation'))
76
79
  conversationArgs = ['--conversation', conversation.id, ...(conversation.session ? ['--resume-session', conversation.session] : [])];
77
80
  }
78
- const launch = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : [])], {
81
+ // A model and effort choice reaches only a guest whose run accepts it (the caller checked).
82
+ const started = await cell.exec.run(['sf-agent', 'run', app, '--name', agent, '--detach', ...conversationArgs, ...(ptyName ? ['--pty-name', ptyName] : []), ...(approvals ? ['--approvals'] : []), ...(setupHash ? ['--setup', '--setup-deps-hash', setupHash] : []), ...runFlags(launch?.choice)], {
79
83
  env: { TERM: term, ...(place ? { SF_PROJECT_NAME: place, SF_REPO_NAME: place } : {}), ...(task ? { SF_TASK_TITLE: task } : {}), ...(setupHash && deferSetup ? { SF_SETUP_DEFERRED: '1' } : {}) }, timeoutMs: 30_000, maxOutputBytes: 4096, signal, ...(cwd ? { cwd } : {}),
80
84
  });
81
- if (launch.exitCode !== 0)
82
- throw new Error(`agent launch failed: ${launch.stderr.trim() || `exit ${launch.exitCode}`}`);
85
+ if (started.exitCode !== 0)
86
+ throw new Error(`agent launch failed: ${started.stderr.trim() || `exit ${started.exitCode}`}`);
83
87
  let result;
84
88
  try {
85
- result = JSON.parse(launch.stdout);
89
+ result = JSON.parse(started.stdout);
86
90
  }
87
91
  catch {
88
92
  throw new Error('agent launcher returned invalid JSON');
@@ -94,6 +98,11 @@ export async function agentPty(cell, app, agent, approvals, term, signal, place,
94
98
  throw new Error(`agent exited before attach: ${session.state}`);
95
99
  return session;
96
100
  }
101
+ /** `sf-agent run <app> --help` of this computer's guest, read at most once per launch. */
102
+ function runHelp(cell, app, signal) {
103
+ let text;
104
+ return () => text ??= cell.exec.run(['sf-agent', 'run', app, '--help'], { timeoutMs: 10_000, maxOutputBytes: 8192, signal }).then(r => r.stdout + r.stderr);
105
+ }
97
106
  /** The first non-empty line of a prompt or title, bounded; the guest makes it the task part of the session title. */
98
107
  export function sessionTask(text) {
99
108
  const line = text?.split(/\r?\n/).map(l => l.replace(/[\s\p{Cc}]+/gu, ' ').trim()).find(Boolean);
@@ -113,9 +122,15 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
113
122
  await prepareInitialCodexProject(cell, cwd, `shard-${ptyName || agent}-${app}`);
114
123
  }
115
124
  const credentialFill = await configureCredentialFill(ctx, ws);
125
+ // This command's --model/--effort, else the conversation's own (a relaunch keeps its model). A computer whose guest
126
+ // predates the choice starts the agent with its defaults, and this command says so once.
127
+ const help = runHelp(cell, app, ctx.signal), explicit = modelChoice(values, app);
128
+ const { choice, applied } = await launchChoice(explicit, conversation?.choice, !!conversation, help);
129
+ if (hasChoice(explicit) && !applied && !ctx.json)
130
+ ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
116
131
  // A cold launch is remembered before it starts, so the listing can show it before the projection does (agent-live.ts).
117
132
  const term = ctx.io.env.TERM && ctx.io.env.TERM !== 'dumb' ? ctx.io.env.TERM : 'xterm-256color';
118
- const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, repositoryName(ws.labels.repo_remote || project), setupHash, cwd, ptyName, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation && { id: conversation.id, session: conversation.session }, sessionTask(typeof values.initialPrompt === 'string' ? values.initialPrompt : conversation?.title));
133
+ const session = await agentPty(cell, app, agent, values.approvals === true, term, ctx.signal, repositoryName(ws.labels.repo_remote || project), setupHash, cwd, ptyName, () => { rememberLaunch(ctx, ws.id); progress(ctx, `Starting ${app === 'claude' ? 'Claude' : 'Codex'} in ${ws.key}…`); }, true, conversation && { id: conversation.id, session: conversation.session }, sessionTask(typeof values.initialPrompt === 'string' ? values.initialPrompt : conversation?.title), { ...(choice ? { choice } : {}), help });
119
134
  // A new conversation's guest can start the agent before this command reaches it, so the launch above never ran.
120
135
  // Remember it either way: listing and `agents open` then read the guest until the projection lists the session.
121
136
  rememberLaunch(ctx, ws.id);
@@ -136,7 +151,9 @@ export async function terminal(ctx, ws, app, agent, project, values, setupHash,
136
151
  if (!attach) {
137
152
  await preparation;
138
153
  // conversation (0.16.0+): which conversation this session is, also when its computer hosts several threads.
139
- ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}) }));
154
+ // model_choice (0.17.0+): the requested model and effort, and whether this thread started with them.
155
+ ctx.io.stdout.write(json({ workspace: ws.data, session, ...(conversation ? { conversation: { id: conversation.id } } : {}), ...(typeof values.initialPrompt === 'string' ? { initial_prompt: { submitted: true } } : {}),
156
+ ...(hasChoice(explicit) ? { model_choice: choiceReport(explicit, applied) } : {}) }));
140
157
  return 0;
141
158
  }
142
159
  ctx.io.stderr.write(`Shardflux · ${ws.key}\nDetach: Ctrl-] then d\n`);
@@ -177,7 +194,9 @@ async function syncCredentials(ctx) {
177
194
  throw new Error('agent credentials sync failed');
178
195
  }
179
196
  }
180
- export const launchProject = async (ctx, values, [path = './', explicitApp]) => {
197
+ export const launchProject = (ctx, values, args) => launchProjectWith(ctx, values, args);
198
+ /** The project VM launch; with `--in t3` a first message becomes a thread on the T3 server in it (SSH mode, 0.17.0+). */
199
+ export async function launchProjectWith(ctx, values, [path = './', explicitApp], thread, install) {
181
200
  if (explicitApp && explicitApp !== 'claude' && explicitApp !== 'codex')
182
201
  throw new UsageError('agent app must be claude or codex');
183
202
  let like = typeof values.like === 'string' ? await resolveWorkspace(ctx, values.like) : undefined;
@@ -189,8 +208,12 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
189
208
  if (target && (!['claude-desktop', 'codex-app', 't3'].includes(target) || !detected?.targets.includes(target)))
190
209
  throw new UsageError(`${target} is not an installed desktop target`);
191
210
  const app = explicitApp ?? store.app(projectKey, like?.labels.app === 'codex' ? 'codex' : 'claude');
192
- if (target === 'codex-app' || (app === 'codex' && !like && values.in === undefined))
211
+ // A typo in --model or --effort is refused before anything starts; T3 Code reads the same values (0.17.0+).
212
+ const choice = modelChoice(values, target === 'codex-app' ? 'codex' : app);
213
+ if (target === 'codex-app' || (app === 'codex' && !like && values.in === undefined && !linuxTerminalSession(ctx, values)))
193
214
  return launchCodexCloud(ctx, values, path);
215
+ if (target === 'claude-desktop' && hasChoice(choice))
216
+ ctx.io.stderr.write('Claude Desktop starts the session with the model chosen in the app.\n');
194
217
  const agent = typeof values.name === 'string' ? values.name : 'main';
195
218
  if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(agent))
196
219
  throw new UsageError('agent name must match ^[a-z0-9][a-z0-9._-]{0,63}$');
@@ -236,8 +259,13 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
236
259
  await queueT3Prewarm(ctx, ws, detected.versions.t3);
237
260
  if (app === 'claude' && !claudeReady)
238
261
  await loginClaudeInWorkspace(ctx, ws);
262
+ // T3 lists both agents; a Codex project still opens when Claude is not signed in (T3 shows Claude as signed out).
239
263
  if (target === 't3' && app === 'codex')
240
- await ensureCloudClaude(ctx, ws);
264
+ await ensureCloudClaude(ctx, ws).catch((error) => {
265
+ if (!(error instanceof UsageError))
266
+ throw error;
267
+ ctx.io.stderr.write(`Claude Code in T3: ${error.message}\n`);
268
+ });
241
269
  if (!(await savedMachine(ctx))?.template)
242
270
  await syncAgentConfig(ctx, ws);
243
271
  // Seed once; two-way sync is scoped to explicit Try locally worktrees.
@@ -251,15 +279,17 @@ export const launchProject = async (ctx, values, [path = './', explicitApp]) =>
251
279
  throw new Error(`project setup could not be queued: ${setup.stderr.trim() || `exit ${setup.exitCode}`}`);
252
280
  }
253
281
  store.saveTarget(projectKey, target);
254
- return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true);
282
+ return openDesktopWorkspace(ctx, ws, target, detected?.versions.t3, true, thread, install);
255
283
  }
256
284
  await prepareAgentMachine(ctx, ws);
257
285
  return await terminal(ctx, ws, app, agent, projectKey, values, prepared?.setupHash);
258
- };
286
+ }
259
287
  export const AGENT_HANDLERS = {
260
288
  async 'agents open'(ctx, values, [agent = 'main']) {
261
289
  if (values.app !== undefined && values.app !== 'claude' && values.app !== 'codex')
262
290
  throw new UsageError('--app must be claude or codex');
291
+ if (process.platform === 'linux' && !ctx.json && !ctx.io.stdin.isTTY && values.detach !== true && (values.in === undefined || values.in === 'terminal'))
292
+ return openLinuxTerminal(ctx, agent, typeof values.app === 'string' ? values.app : undefined);
263
293
  let resolvedConversation;
264
294
  if (/^conv_[a-z0-9]{26}$/.test(agent)) {
265
295
  resolvedConversation = await getConversation(ctx, agent);
@@ -274,7 +304,27 @@ export const AGENT_HANDLERS = {
274
304
  const store = new AgentStateStore(ctx.store.dir);
275
305
  const app = values.app ?? resolvedConversation?.harness ?? ws.labels.shard_app ?? ws.labels.app ?? store.app(project);
276
306
  const detected = installedTargets(ctx.io.env);
277
- const saved = store.read().projects?.[project]?.target ??
307
+ // open_in (0.17.0+): a choice saved with `agents targets --open-in`, or T3 when it is the default, comes first.
308
+ // A conversation opens as itself (its id and session), as the fallbacks below open it.
309
+ const choice = app === 'claude' || app === 'codex' ? openInFor(ctx, ws.labels.shard_project || ws.key.split('/')[0], { clients: detected.clients })[app] : undefined;
310
+ const chosen = choice && (choice.saved || choice.target === 't3') ? choice.target : undefined;
311
+ const opened = resolvedConversation?.id ?? ws.key;
312
+ if (chosen === 't3' || chosen === 'web') {
313
+ const { HANDLERS } = await import("./handlers.js");
314
+ return HANDLERS[`agents open ${chosen}`](ctx, { ...values, in: chosen }, [opened]);
315
+ }
316
+ if (chosen === 'claude-desktop' && app === 'claude')
317
+ return openClaudeDesktopSession(ctx, ws, resolvedConversation);
318
+ if (chosen === 'terminal' && app === 'claude') {
319
+ if (ctx.json) {
320
+ out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
321
+ return 0;
322
+ }
323
+ const next = { ...values };
324
+ delete next.in;
325
+ return AGENT_HANDLERS['agents open'](ctx, next, [opened]);
326
+ }
327
+ const saved = chosen === 'codex-app' ? undefined : store.read().projects?.[project]?.target ??
278
328
  (project.startsWith('file://') ? store.read().projects?.[new URL(project).pathname]?.target : undefined);
279
329
  // Codex's default open implementation owns Cloud/desktop routing. PAS-XB replaces that path; do not pin
280
330
  // the menu to the old per-VM SSH target. An explicit saved desktop target still takes precedence.
@@ -295,7 +345,6 @@ export const AGENT_HANDLERS = {
295
345
  if (detected.clients.includes('claude-desktop')) {
296
346
  return openClaudeDesktopSession(ctx, ws, resolvedConversation);
297
347
  }
298
- const opened = resolvedConversation?.id ?? ws.key;
299
348
  if (ctx.json) {
300
349
  // An app runs this in the person's terminal (the menu bar opens Terminal with it).
301
350
  out(ctx, `shard agents open ${opened}\n`, { target: 'terminal', workspace: ws.data, command: ['shard', 'agents', 'open', opened] });
@@ -0,0 +1,61 @@
1
+ import type { Values } from './args.js';
2
+ export type ModelApp = 'claude' | 'codex';
3
+ export interface Choice {
4
+ id: string;
5
+ label: string;
6
+ }
7
+ /**
8
+ * `label` is the menu text, `short` the pill text and `description` what the model is for. A model may take fewer
9
+ * effort levels than its app (its own `efforts`, ids); without it, all of the app's.
10
+ */
11
+ export interface ModelOption extends Choice {
12
+ short: string;
13
+ description?: string;
14
+ efforts?: string[];
15
+ }
16
+ /**
17
+ * `default` names what the agent starts with when no flag is sent, as concrete ids, or null where the agent decides at
18
+ * start: Claude Code resolves its default model and effort from the account's plan and organization, and Codex its
19
+ * default effort from the model catalog it fetches with the account's login. A Codex Cloud thread starts with
20
+ * gpt-6.1-sol (services/guest-agent/internal/codexcloud CloudModel), which is also Codex 0.160.0's first model.
21
+ */
22
+ export interface AppModels {
23
+ models: ModelOption[];
24
+ efforts: Choice[];
25
+ default: {
26
+ model: string | null;
27
+ effort: string | null;
28
+ };
29
+ }
30
+ export declare const DEFAULT_CHOICE = "default";
31
+ /** Claude Code `--model <alias>` and `--effort <level>`; Codex `-m <model>` and `-c model_reasoning_effort=<level>`. */
32
+ export declare const MODEL_CHOICES: Readonly<Record<ModelApp, AppModels>>;
33
+ /** What a thread starts with: absent fields are the agent's own default (no flag). */
34
+ export interface ModelChoice {
35
+ model?: string;
36
+ effort?: string;
37
+ }
38
+ /** `--model` and `--effort` for this app, checked against its list. Throws a usage error for anything else. */
39
+ export declare function modelChoice(values: Values, app: ModelApp): ModelChoice;
40
+ export declare const hasChoice: (c: ModelChoice | undefined) => c is ModelChoice;
41
+ /** The guest's `sf-agent run` flags for a choice (sf-agent maps them to the agent's own flags). */
42
+ export declare function runFlags(c: ModelChoice | undefined): string[];
43
+ /** Whether a guest's `sf-agent run --help` lists the model and effort flags (coding templates from train 14). */
44
+ export declare const runAcceptsChoice: (help: string) => boolean;
45
+ /**
46
+ * The choice a launch passes (this command's, else the conversation's recorded one, so a relaunch keeps its model) and
47
+ * whether the thread starts with this command's choice: the computer's guest accepts it and, for a conversation, its
48
+ * record carries it (the guest starts a conversation's agent from the record).
49
+ */
50
+ export declare function launchChoice(explicit: ModelChoice, recorded: ModelChoice | undefined, conversation: boolean, help: () => Promise<string>): Promise<{
51
+ choice?: ModelChoice;
52
+ applied: boolean;
53
+ }>;
54
+ /** Said once when a computer's platform predates the choice: the thread starts with the agent's defaults. */
55
+ export declare const CHOICE_PENDING = "The model and effort choice applies to new threads after the next platform update; this thread uses the defaults.";
56
+ /** The record the JSON output carries for a requested choice. */
57
+ export declare function choiceReport(c: ModelChoice, applied: boolean): {
58
+ model: string;
59
+ effort: string;
60
+ applied: boolean;
61
+ };
@@ -0,0 +1,79 @@
1
+ import { UsageError } from "./errors.js";
2
+ export const DEFAULT_CHOICE = 'default';
3
+ const DEFAULT_MODEL = { id: DEFAULT_CHOICE, label: 'Default', short: 'Default' };
4
+ const LEVELS = { low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', max: 'Max', ultra: 'Ultra' };
5
+ const levels = (...ids) => [{ id: DEFAULT_CHOICE, label: 'Default' }, ...ids.map(id => ({ id, label: LEVELS[id] }))];
6
+ /** Claude Code `--model <alias>` and `--effort <level>`; Codex `-m <model>` and `-c model_reasoning_effort=<level>`. */
7
+ export const MODEL_CHOICES = {
8
+ claude: {
9
+ // The aliases always name the latest model of each family; descriptions are Claude Code's own.
10
+ models: [DEFAULT_MODEL,
11
+ { id: 'fable', label: 'Fable', short: 'Fable', description: 'For your toughest challenges' },
12
+ { id: 'opus', label: 'Opus', short: 'Opus', description: 'Most capable for ambitious work' },
13
+ { id: 'sonnet', label: 'Sonnet', short: 'Sonnet', description: 'Most efficient for everyday tasks' },
14
+ { id: 'haiku', label: 'Haiku', short: 'Haiku', description: 'Fastest for quick answers' }],
15
+ efforts: levels('low', 'medium', 'high', 'xhigh', 'max'),
16
+ default: { model: null, effort: null },
17
+ },
18
+ codex: {
19
+ // Descriptions are Codex's own model catalog.
20
+ models: [DEFAULT_MODEL,
21
+ { id: 'gpt-6.1-sol', label: 'GPT-6.1 Sol', short: 'GPT-6.1 Sol', description: 'Latest workhorse model for coding and everyday work' },
22
+ { id: 'gpt-6-astra', label: 'GPT-6 Astra', short: 'GPT-6 Astra', description: 'Frontier intelligence for the most demanding work' },
23
+ { id: 'gpt-6-luna', label: 'GPT-6 Luna', short: 'GPT-6 Luna', description: 'Fast and affordable model for easier tasks', efforts: [DEFAULT_CHOICE, 'low', 'medium', 'high', 'xhigh', 'max'] }],
24
+ efforts: levels('low', 'medium', 'high', 'xhigh', 'max', 'ultra'),
25
+ default: { model: 'gpt-6.1-sol', effort: null },
26
+ },
27
+ };
28
+ const list = (ids) => ids.length > 1 ? `${ids.slice(0, -1).join(', ')} or ${ids.at(-1)}` : ids[0];
29
+ const label = (app) => app === 'claude' ? 'Claude' : 'Codex';
30
+ /** `--model` and `--effort` for this app, checked against its list. Throws a usage error for anything else. */
31
+ export function modelChoice(values, app) {
32
+ const raw = (name) => {
33
+ const value = values[name];
34
+ if (value === undefined)
35
+ return undefined;
36
+ if (typeof value !== 'string' || !value.trim())
37
+ throw new UsageError(`--${name} needs a value.`);
38
+ return value.trim().toLowerCase();
39
+ };
40
+ const model = raw('model'), effort = raw('effort'), choices = MODEL_CHOICES[app];
41
+ const option = model === undefined ? undefined : choices.models.find(m => m.id === model);
42
+ if (model !== undefined && !option)
43
+ throw new UsageError(`${label(app)} models: ${list(choices.models.map(m => m.id))}.`);
44
+ const allowed = option?.efforts ?? choices.efforts.map(e => e.id);
45
+ if (effort !== undefined && !allowed.includes(effort)) {
46
+ throw new UsageError(`${option?.efforts ? option.label : label(app)} effort levels: ${list(allowed)}.`);
47
+ }
48
+ return {
49
+ ...(model && model !== DEFAULT_CHOICE ? { model } : {}),
50
+ ...(effort && effort !== DEFAULT_CHOICE ? { effort } : {}),
51
+ };
52
+ }
53
+ export const hasChoice = (c) => !!(c?.model || c?.effort);
54
+ /** The guest's `sf-agent run` flags for a choice (sf-agent maps them to the agent's own flags). */
55
+ export function runFlags(c) {
56
+ return [...(c?.model ? ['--model', c.model] : []), ...(c?.effort ? ['--effort', c.effort] : [])];
57
+ }
58
+ /** Whether a guest's `sf-agent run --help` lists the model and effort flags (coding templates from train 14). */
59
+ export const runAcceptsChoice = (help) => /-model\b/.test(help) && /-effort\b/.test(help);
60
+ /**
61
+ * The choice a launch passes (this command's, else the conversation's recorded one, so a relaunch keeps its model) and
62
+ * whether the thread starts with this command's choice: the computer's guest accepts it and, for a conversation, its
63
+ * record carries it (the guest starts a conversation's agent from the record).
64
+ */
65
+ export async function launchChoice(explicit, recorded, conversation, help) {
66
+ const choice = hasChoice(explicit) ? explicit : recorded;
67
+ if (!hasChoice(choice))
68
+ return { applied: true };
69
+ // A guest that cannot say what it accepts gets no flags: the thread starts either way.
70
+ const accepted = runAcceptsChoice(await help().catch(() => ''));
71
+ const kept = !conversation || !hasChoice(explicit) || (recorded?.model === explicit.model && recorded?.effort === explicit.effort);
72
+ return { ...(accepted ? { choice } : {}), applied: accepted && kept };
73
+ }
74
+ /** Said once when a computer's platform predates the choice: the thread starts with the agent's defaults. */
75
+ export const CHOICE_PENDING = 'The model and effort choice applies to new threads after the next platform update; this thread uses the defaults.';
76
+ /** The record the JSON output carries for a requested choice. */
77
+ export function choiceReport(c, applied) {
78
+ return { model: c.model ?? DEFAULT_CHOICE, effort: c.effort ?? DEFAULT_CHOICE, applied };
79
+ }
@@ -12,6 +12,16 @@ export declare class AgentStateStore {
12
12
  read(): State;
13
13
  app(project: string, fallback?: 'claude' | 'codex'): 'claude' | 'codex';
14
14
  target(project: string, available: string[]): string;
15
+ /** T3 Code environments set up from this computer (the SSH route): targets.json keeps T3 out of its manifest. */
16
+ t3Environments(apiUrl: string): Array<{
17
+ workspace: string;
18
+ host: string;
19
+ }>;
20
+ saveT3Environment(entry: {
21
+ workspace: string;
22
+ host: string;
23
+ api_url: string;
24
+ }): void;
15
25
  saveTarget(project: string, target: string): void;
16
26
  update(change: (state: State) => void): void;
17
27
  save(project: string, app: 'claude' | 'codex'): void;
@@ -28,6 +28,19 @@ export class AgentStateStore {
28
28
  const target = this.read().projects?.[project]?.target;
29
29
  return target && available.includes(target) ? target : available.includes('t3') ? 't3' : 'terminal';
30
30
  }
31
+ /** T3 Code environments set up from this computer (the SSH route): targets.json keeps T3 out of its manifest. */
32
+ t3Environments(apiUrl) {
33
+ const list = this.read().t3;
34
+ return (Array.isArray(list) ? list : []).filter((e) => !!e && typeof e === 'object' && typeof e.workspace === 'string' && typeof e.host === 'string'
35
+ && e.api_url === apiUrl).map(({ workspace, host }) => ({ workspace, host }));
36
+ }
37
+ saveT3Environment(entry) {
38
+ this.update(doc => {
39
+ const list = (Array.isArray(doc.t3) ? doc.t3 : []);
40
+ // The newest last: the onboarding card names the one most recently set up.
41
+ doc.t3 = [...list.filter(e => !(e.host === entry.host && e.api_url === entry.api_url)), entry];
42
+ });
43
+ }
31
44
  saveTarget(project, target) {
32
45
  this.update(doc => {
33
46
  doc.projects ??= {};
@@ -4,6 +4,7 @@ import type { Conversation } from './conversations.js';
4
4
  import type { Ctx, Handler, Io } from './commands.js';
5
5
  import { AgentTargets } from './agent-targets.js';
6
6
  import type { DesktopTarget } from './agent-targets.js';
7
+ import type { T3Install, T3ThreadRequest } from './t3-mode.js';
7
8
  /** Optimistic file replacement preserves keys installed by other devices connecting at the same time. */
8
9
  export declare function installDeviceKey(cell: CellClient, key: string, signal?: AbortSignal): Promise<void>;
9
10
  /** Host trust is established over the authenticated files API, before sending any SSH protocol bytes. */
@@ -27,6 +28,10 @@ export declare const CLAUDE_DESKTOP_BUNDLE = "com.anthropic.claudefordesktop";
27
28
  * opens a Claude conversation when Claude Desktop is installed. --json prints {target, url, app_bundle_id} for apps.
28
29
  */
29
30
  export declare function openClaudeDesktopSession(ctx: Ctx, ws: Workspace, conversation?: Pick<Conversation, 'harness_session_id'>): Promise<number>;
30
- export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean): Promise<number>;
31
+ /**
32
+ * Opens a workspace as a desktop app's SSH environment. For T3 (0.17.0+) a first message becomes a thread on the T3
33
+ * server in the VM (SSH mode), and T3 is brought to the front with --json too (`opened: true`).
34
+ */
35
+ export declare function openDesktopWorkspace(ctx: Ctx, ws: Workspace, target: DesktopTarget, t3Version?: string, prewarmed?: boolean, thread?: T3ThreadRequest, install?: T3Install, conversation?: string | null): Promise<number>;
31
36
  export declare function queueT3Prewarm(ctx: Ctx, ws: Workspace, version: string): Promise<void>;
32
37
  export declare const TARGET_HANDLERS: Record<string, Handler>;