t3code-cli 0.10.0 → 0.11.0

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 (124) hide show
  1. package/README.md +33 -1
  2. package/dist/application.js +2 -2
  3. package/dist/auth.js +1 -1
  4. package/dist/bin.js +1566 -351
  5. package/dist/cli.js +1 -1
  6. package/dist/config.js +1 -1
  7. package/dist/connection.js +1 -11
  8. package/dist/index.js +1 -1
  9. package/dist/layout.js +1 -1
  10. package/dist/node.js +2 -0
  11. package/dist/orchestration.js +2 -2
  12. package/dist/rpc.js +2 -2
  13. package/dist/runtime.js +1 -1
  14. package/dist/scope.js +1 -1
  15. package/dist/shared.js +1740 -767
  16. package/dist/src/application/index.d.ts +3 -2
  17. package/dist/src/application/layer.d.ts +82 -841
  18. package/dist/src/application/model-selection.d.ts +15 -0
  19. package/dist/src/application/models.d.ts +1 -87
  20. package/dist/src/application/project-commands.d.ts +9 -0
  21. package/dist/src/application/projects.d.ts +13 -135
  22. package/dist/src/application/service.d.ts +84 -5
  23. package/dist/src/application/shell-sequence.d.ts +2 -12
  24. package/dist/src/application/terminals.d.ts +66 -0
  25. package/dist/src/application/thread-commands.d.ts +47 -0
  26. package/dist/src/application/thread-update.d.ts +8 -0
  27. package/dist/src/application/thread-wait.d.ts +3 -14
  28. package/dist/src/application/threads.d.ts +21 -50
  29. package/dist/src/cli/flags.d.ts +3 -0
  30. package/dist/src/cli/output-format.d.ts +2 -0
  31. package/dist/src/domain/error.d.ts +14 -1
  32. package/dist/src/domain/helpers.d.ts +0 -9
  33. package/dist/src/domain/model-config.d.ts +29 -3
  34. package/dist/src/domain/thread-lifecycle.d.ts +2 -10
  35. package/dist/src/node/index.d.ts +2 -0
  36. package/dist/src/orchestration/index.d.ts +1 -0
  37. package/dist/src/orchestration/layer.d.ts +177 -1032
  38. package/dist/src/orchestration/service.d.ts +1 -0
  39. package/dist/src/rpc/error.d.ts +4 -2
  40. package/dist/src/rpc/index.d.ts +5 -0
  41. package/dist/src/rpc/layer.d.ts +1097 -499
  42. package/dist/src/rpc/operation.d.ts +5752 -0
  43. package/dist/src/rpc/ws-group.d.ts +327 -147
  44. package/dist/src/runtime/layer.d.ts +1 -1
  45. package/dist/t3tools.js +2 -2
  46. package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +10 -33
  47. package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +0 -6
  48. package/dist/upstream-t3code/packages/contracts/src/orchestration.d.ts +1 -657
  49. package/dist/upstream-t3code/packages/contracts/src/provider.d.ts +0 -14
  50. package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +1 -175
  51. package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +0 -492
  52. package/dist/upstream-t3code/packages/contracts/src/server.d.ts +0 -45
  53. package/dist/upstream-t3code/packages/contracts/src/settings.d.ts +0 -21
  54. package/dist/upstream-t3code/packages/contracts/src/terminal.d.ts +3 -6
  55. package/package.json +6 -2
  56. package/src/application/index.ts +28 -2
  57. package/src/application/layer.ts +34 -5
  58. package/src/application/model-selection.ts +68 -2
  59. package/src/application/models.ts +14 -14
  60. package/src/application/project-commands.ts +15 -0
  61. package/src/application/projects.ts +43 -13
  62. package/src/application/service.ts +196 -77
  63. package/src/application/shell-sequence.ts +5 -7
  64. package/src/application/terminals.ts +207 -0
  65. package/src/application/thread-commands.test.ts +43 -0
  66. package/src/application/thread-commands.ts +81 -0
  67. package/src/application/thread-update.ts +72 -0
  68. package/src/application/thread-wait.ts +46 -47
  69. package/src/application/threads.test.ts +166 -0
  70. package/src/application/threads.ts +123 -40
  71. package/src/bin.ts +2 -0
  72. package/src/cli/app.ts +2 -0
  73. package/src/cli/confirm.ts +31 -0
  74. package/src/cli/error.ts +26 -3
  75. package/src/cli/flags.ts +15 -0
  76. package/src/cli/input/layer.ts +20 -0
  77. package/src/cli/input/service.ts +1 -0
  78. package/src/cli/output-format.ts +6 -2
  79. package/src/cli/project-format.ts +7 -0
  80. package/src/cli/project.ts +2 -1
  81. package/src/cli/projects/delete.ts +50 -0
  82. package/src/cli/self-action.ts +34 -0
  83. package/src/cli/terminal/attach.ts +32 -0
  84. package/src/cli/terminal/commands.test.ts +101 -0
  85. package/src/cli/terminal/create.ts +54 -0
  86. package/src/cli/terminal/destroy.ts +60 -0
  87. package/src/cli/terminal/encoding.test.ts +63 -0
  88. package/src/cli/terminal/encoding.ts +23 -0
  89. package/src/cli/terminal/error.ts +14 -0
  90. package/src/cli/terminal/io-node-layer.ts +82 -0
  91. package/src/cli/terminal/io-service.ts +25 -0
  92. package/src/cli/terminal/list.ts +35 -0
  93. package/src/cli/terminal/read.ts +102 -0
  94. package/src/cli/terminal/scope.ts +30 -0
  95. package/src/cli/terminal/shared.ts +250 -0
  96. package/src/cli/terminal/stream.ts +64 -0
  97. package/src/cli/terminal/wait.test.ts +146 -0
  98. package/src/cli/terminal/wait.ts +222 -0
  99. package/src/cli/terminal/write.ts +132 -0
  100. package/src/cli/terminal-format.ts +167 -0
  101. package/src/cli/terminal.ts +26 -0
  102. package/src/cli/thread-format.test.ts +32 -0
  103. package/src/cli/thread-format.ts +8 -1
  104. package/src/cli/thread.ts +8 -0
  105. package/src/cli/threads/archive.ts +11 -18
  106. package/src/cli/threads/delete.ts +61 -0
  107. package/src/cli/threads/interrupt.ts +52 -0
  108. package/src/cli/threads/list.test.ts +69 -0
  109. package/src/cli/threads/list.ts +15 -3
  110. package/src/cli/threads/send.ts +22 -2
  111. package/src/cli/threads/unarchive.ts +44 -0
  112. package/src/cli/threads/update.ts +150 -0
  113. package/src/domain/error.ts +20 -1
  114. package/src/domain/model-config.ts +4 -0
  115. package/src/domain/thread-lifecycle.ts +14 -0
  116. package/src/node/index.ts +2 -0
  117. package/src/orchestration/index.ts +1 -0
  118. package/src/orchestration/layer.ts +33 -85
  119. package/src/orchestration/service.ts +4 -0
  120. package/src/rpc/error.ts +10 -0
  121. package/src/rpc/index.ts +5 -0
  122. package/src/rpc/operation.ts +83 -0
  123. package/src/rpc/ws-group.ts +24 -0
  124. package/src/runtime/layer.ts +7 -2
@@ -11,6 +11,10 @@ import type {
11
11
  OrchestrationThreadShell,
12
12
  ProviderUserInputAnswers,
13
13
  ServerProvider,
14
+ TerminalAttachStreamEvent,
15
+ TerminalMetadataStreamEvent,
16
+ TerminalSessionSnapshot,
17
+ TerminalSummary,
14
18
  } from "#t3tools/contracts";
15
19
 
16
20
  import type { ApplicationError } from "./error.ts";
@@ -26,6 +30,22 @@ export type StartThreadInput = {
26
30
  readonly worktreePath?: string;
27
31
  };
28
32
 
33
+ export type CreateTerminalInput = {
34
+ readonly threadId: string;
35
+ readonly terminalId?: string;
36
+ readonly command?: string;
37
+ };
38
+
39
+ export type TerminalRef = {
40
+ readonly threadId: string;
41
+ readonly terminalId: string;
42
+ };
43
+
44
+ export type TerminalAttachTarget = TerminalRef & {
45
+ readonly cwd: string;
46
+ readonly worktreePath: string | null;
47
+ };
48
+
29
49
  export type SendThreadInput = {
30
50
  readonly threadId: string;
31
51
  readonly message: string;
@@ -38,6 +58,18 @@ export type CallbackThreadInput = {
38
58
  readonly prompt: string;
39
59
  };
40
60
 
61
+ export type ListThreadsInclude = "active" | "archived" | "all";
62
+
63
+ export type UpdateThreadInput = {
64
+ readonly threadId: string;
65
+ readonly title?: string;
66
+ readonly provider?: string;
67
+ readonly model?: string;
68
+ readonly options?: NonNullable<ModelSelection["options"]>;
69
+ readonly branch?: string | null;
70
+ readonly worktreePath?: string | null;
71
+ };
72
+
41
73
  export type StartThreadPolicy = {
42
74
  readonly until: "dispatch" | "visible" | "complete";
43
75
  };
@@ -48,82 +80,169 @@ export type WaitEvent =
48
80
  | { readonly type: "status"; readonly status: string; readonly threadId: string }
49
81
  | { readonly type: "done"; readonly thread: OrchestrationThread };
50
82
 
51
- export class T3Application extends Context.Service<
52
- T3Application,
53
- {
54
- readonly loadShell: () => Effect.Effect<OrchestrationShellSnapshot, ApplicationError>;
55
- readonly listModels: (input: {
56
- readonly all?: boolean;
57
- readonly provider?: string;
58
- }) => Effect.Effect<ReadonlyArray<ServerProvider>, ApplicationError>;
59
- readonly addProject: (input: {
60
- readonly path: string;
61
- readonly title?: string;
62
- }) => Effect.Effect<
63
- { readonly dispatch: DispatchResult; readonly project: OrchestrationProjectShell },
64
- ApplicationError
65
- >;
66
- readonly listThreads: (projectRef: string) => Effect.Effect<
67
- {
68
- readonly project: OrchestrationProjectShell;
69
- readonly threads: ReadonlyArray<OrchestrationThreadShell>;
70
- },
71
- ApplicationError
72
- >;
73
- readonly getThreadMessages: (
74
- threadId: string,
75
- ) => Effect.Effect<OrchestrationThread, ApplicationError>;
76
- readonly showThread: (threadId: string) => Effect.Effect<ThreadShow, ApplicationError>;
77
- readonly approveThread: (input: {
83
+ export type T3ModelApplicationService = {
84
+ readonly listModels: (input: {
85
+ readonly all?: boolean;
86
+ readonly provider?: string;
87
+ }) => Effect.Effect<ReadonlyArray<ServerProvider>, ApplicationError>;
88
+ };
89
+
90
+ export class T3ModelApplication extends Context.Service<
91
+ T3ModelApplication,
92
+ T3ModelApplicationService
93
+ >()("t3cli/T3ModelApplication") {}
94
+
95
+ export type T3ProjectApplicationService = {
96
+ readonly loadShell: () => Effect.Effect<OrchestrationShellSnapshot, ApplicationError>;
97
+ readonly addProject: (input: {
98
+ readonly path: string;
99
+ readonly title?: string;
100
+ }) => Effect.Effect<
101
+ { readonly dispatch: DispatchResult; readonly project: OrchestrationProjectShell },
102
+ ApplicationError
103
+ >;
104
+ readonly resolveProject: (
105
+ projectRef: string,
106
+ ) => Effect.Effect<OrchestrationProjectShell, ApplicationError>;
107
+ readonly deleteProject: (input: {
108
+ readonly projectId: string;
109
+ readonly force?: boolean;
110
+ }) => Effect.Effect<
111
+ { readonly projectId: string; readonly dispatch: DispatchResult },
112
+ ApplicationError
113
+ >;
114
+ };
115
+
116
+ export class T3ProjectApplication extends Context.Service<
117
+ T3ProjectApplication,
118
+ T3ProjectApplicationService
119
+ >()("t3cli/T3ProjectApplication") {}
120
+
121
+ export type T3ThreadApplicationService = {
122
+ readonly listThreads: (
123
+ projectRef: string,
124
+ options?: {
125
+ readonly include?: ListThreadsInclude;
126
+ },
127
+ ) => Effect.Effect<
128
+ {
129
+ readonly project: OrchestrationProjectShell;
130
+ readonly threads: ReadonlyArray<OrchestrationThreadShell>;
131
+ },
132
+ ApplicationError
133
+ >;
134
+ readonly getThreadMessages: (
135
+ threadId: string,
136
+ ) => Effect.Effect<OrchestrationThread, ApplicationError>;
137
+ readonly showThread: (threadId: string) => Effect.Effect<ThreadShow, ApplicationError>;
138
+ readonly approveThread: (input: {
139
+ readonly threadId: string;
140
+ readonly requestId: string;
141
+ readonly decision: "accept" | "decline" | "cancel";
142
+ }) => Effect.Effect<
143
+ { readonly threadId: string; readonly requestId: string; readonly dispatch: DispatchResult },
144
+ ApplicationError
145
+ >;
146
+ readonly respondToThread: (input: {
147
+ readonly threadId: string;
148
+ readonly requestId: string;
149
+ readonly answers: ProviderUserInputAnswers;
150
+ }) => Effect.Effect<
151
+ { readonly threadId: string; readonly requestId: string; readonly dispatch: DispatchResult },
152
+ ApplicationError
153
+ >;
154
+ readonly archiveThread: (threadId: string) => Effect.Effect<DispatchResult, ApplicationError>;
155
+ readonly interruptThread: (threadId: string) => Effect.Effect<DispatchResult, ApplicationError>;
156
+ readonly unarchiveThread: (threadId: string) => Effect.Effect<DispatchResult, ApplicationError>;
157
+ readonly deleteThread: (
158
+ threadId: string,
159
+ ) => Effect.Effect<
160
+ { readonly threadId: string; readonly dispatch: DispatchResult },
161
+ ApplicationError
162
+ >;
163
+ readonly updateThread: (
164
+ input: UpdateThreadInput,
165
+ ) => Effect.Effect<DispatchResult, ApplicationError>;
166
+ readonly startThread: (
167
+ input: StartThreadInput,
168
+ policy?: StartThreadPolicy,
169
+ ) => Effect.Effect<
170
+ {
171
+ readonly dispatch: DispatchResult;
172
+ readonly project: OrchestrationProjectShell;
78
173
  readonly threadId: string;
79
- readonly requestId: string;
80
- readonly decision: "accept" | "decline" | "cancel";
81
- }) => Effect.Effect<
82
- { readonly threadId: string; readonly requestId: string; readonly dispatch: DispatchResult },
83
- ApplicationError
84
- >;
85
- readonly respondToThread: (input: {
174
+ readonly thread?: OrchestrationThread;
175
+ },
176
+ ApplicationError
177
+ >;
178
+ readonly sendThread: (
179
+ input: SendThreadInput,
180
+ policy?: StartThreadPolicy,
181
+ ) => Effect.Effect<
182
+ {
183
+ readonly dispatch: DispatchResult;
86
184
  readonly threadId: string;
87
- readonly requestId: string;
88
- readonly answers: ProviderUserInputAnswers;
89
- }) => Effect.Effect<
90
- { readonly threadId: string; readonly requestId: string; readonly dispatch: DispatchResult },
91
- ApplicationError
92
- >;
93
- readonly archiveThread: (threadId: string) => Effect.Effect<DispatchResult, ApplicationError>;
94
- readonly startThread: (
95
- input: StartThreadInput,
96
- policy?: StartThreadPolicy,
97
- ) => Effect.Effect<
98
- {
99
- readonly dispatch: DispatchResult;
100
- readonly project: OrchestrationProjectShell;
101
- readonly threadId: string;
102
- readonly thread?: OrchestrationThread;
103
- },
104
- ApplicationError
105
- >;
106
- readonly sendThread: (
107
- input: SendThreadInput,
108
- policy?: StartThreadPolicy,
109
- ) => Effect.Effect<
110
- {
111
- readonly dispatch: DispatchResult;
112
- readonly threadId: string;
113
- readonly thread?: OrchestrationThread;
114
- },
115
- ApplicationError
116
- >;
117
- readonly watchThread: (threadId: string) => Stream.Stream<WaitEvent, ApplicationError>;
118
- readonly waitForThread: (
119
- threadId: string,
120
- ) => Effect.Effect<OrchestrationThread, ApplicationError>;
121
- readonly callbackThread: (input: CallbackThreadInput) => Effect.Effect<
122
- {
123
- readonly dispatch: DispatchResult;
124
- readonly targetThreadId: string;
125
- },
126
- ApplicationError
127
- >;
128
- }
129
- >()("t3cli/T3Application") {}
185
+ readonly thread?: OrchestrationThread;
186
+ },
187
+ ApplicationError
188
+ >;
189
+ readonly watchThread: (threadId: string) => Stream.Stream<WaitEvent, ApplicationError>;
190
+ readonly waitForThread: (
191
+ threadId: string,
192
+ ) => Effect.Effect<OrchestrationThread, ApplicationError>;
193
+ readonly callbackThread: (input: CallbackThreadInput) => Effect.Effect<
194
+ {
195
+ readonly dispatch: DispatchResult;
196
+ readonly targetThreadId: string;
197
+ },
198
+ ApplicationError
199
+ >;
200
+ };
201
+
202
+ export class T3ThreadApplication extends Context.Service<
203
+ T3ThreadApplication,
204
+ T3ThreadApplicationService
205
+ >()("t3cli/T3ThreadApplication") {}
206
+
207
+ export type T3TerminalApplicationService = {
208
+ readonly listTerminals: (
209
+ threadId: string,
210
+ ) => Effect.Effect<ReadonlyArray<TerminalSummary>, ApplicationError>;
211
+ readonly getTerminal: (terminal: TerminalRef) => Effect.Effect<TerminalSummary, ApplicationError>;
212
+ readonly createTerminal: (
213
+ input: CreateTerminalInput,
214
+ ) => Effect.Effect<TerminalSessionSnapshot, ApplicationError>;
215
+ readonly attachTerminal: (input: {
216
+ readonly terminal: TerminalAttachTarget;
217
+ readonly cols?: number;
218
+ readonly rows?: number;
219
+ }) => Stream.Stream<TerminalAttachStreamEvent, ApplicationError>;
220
+ readonly watchTerminalMetadata: () => Stream.Stream<
221
+ TerminalMetadataStreamEvent,
222
+ ApplicationError
223
+ >;
224
+ readonly writeTerminal: (input: {
225
+ readonly terminal: TerminalRef;
226
+ readonly data: string;
227
+ }) => Effect.Effect<void, ApplicationError>;
228
+ readonly resizeTerminal: (input: {
229
+ readonly terminal: TerminalRef;
230
+ readonly cols: number;
231
+ readonly rows: number;
232
+ }) => Effect.Effect<void, ApplicationError>;
233
+ readonly destroyTerminal: (terminal: TerminalRef) => Effect.Effect<void, ApplicationError>;
234
+ };
235
+
236
+ export class T3TerminalApplication extends Context.Service<
237
+ T3TerminalApplication,
238
+ T3TerminalApplicationService
239
+ >()("t3cli/T3TerminalApplication") {}
240
+
241
+ export type T3ApplicationService = T3ModelApplicationService &
242
+ T3ProjectApplicationService &
243
+ T3ThreadApplicationService &
244
+ T3TerminalApplicationService;
245
+
246
+ export class T3Application extends Context.Service<T3Application, T3ApplicationService>()(
247
+ "t3cli/T3Application",
248
+ ) {}
@@ -3,15 +3,13 @@ import * as Option from "effect/Option";
3
3
  import * as Stream from "effect/Stream";
4
4
  import { ORCHESTRATION_WS_METHODS } from "#t3tools/contracts";
5
5
 
6
- import type { Orchestration } from "../orchestration/service.ts";
6
+ import { T3Orchestration } from "../orchestration/service.ts";
7
7
  import { RpcError } from "../rpc/error.ts";
8
8
 
9
- export function waitForShellSequence(input: {
10
- readonly orchestration: Orchestration;
11
- readonly sequence: number;
12
- }) {
9
+ export function waitForShellSequence(input: { readonly sequence: number }) {
13
10
  return Effect.gen(function* () {
14
- const sequence = yield* input.orchestration.watchShellSequence().pipe(
11
+ const orchestration = yield* T3Orchestration;
12
+ const sequence = yield* orchestration.watchShellSequence().pipe(
15
13
  Stream.filter((snapshotSequence) => snapshotSequence >= input.sequence),
16
14
  Stream.runHead,
17
15
  Effect.scoped,
@@ -24,6 +22,6 @@ export function waitForShellSequence(input: {
24
22
  }),
25
23
  );
26
24
  }
27
- return yield* input.orchestration.getShellSnapshot();
25
+ return yield* orchestration.getShellSnapshot();
28
26
  });
29
27
  }
@@ -0,0 +1,207 @@
1
+ import * as Crypto from "effect/Crypto";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
4
+ import * as Stream from "effect/Stream";
5
+ import { WS_METHODS, type TerminalSummary } from "#t3tools/contracts";
6
+
7
+ import { ProjectLookupError, TerminalLookupError, ThreadLookupError } from "../domain/error.ts";
8
+ import { findProjectById } from "../domain/helpers.ts";
9
+ import { T3Orchestration } from "../orchestration/service.ts";
10
+ import { RpcError } from "../rpc/error.ts";
11
+ import { T3RpcOperations } from "../rpc/operation.ts";
12
+ import type { T3TerminalApplicationService, CreateTerminalInput, TerminalRef } from "./service.ts";
13
+
14
+ export const makeTerminalApplication = Effect.fn("makeTerminalApplication")(function* () {
15
+ const crypto = yield* Crypto.Crypto;
16
+ const orchestration = yield* T3Orchestration;
17
+ const rpc = yield* T3RpcOperations;
18
+
19
+ const attachTerminal: T3TerminalApplicationService["attachTerminal"] = (input) =>
20
+ rpc.subscribe(WS_METHODS.terminalAttach, (client) =>
21
+ client[WS_METHODS.terminalAttach]({
22
+ threadId: input.terminal.threadId,
23
+ terminalId: input.terminal.terminalId,
24
+ cwd: input.terminal.cwd,
25
+ worktreePath: input.terminal.worktreePath,
26
+ ...(input.cols !== undefined ? { cols: input.cols } : {}),
27
+ ...(input.rows !== undefined ? { rows: input.rows } : {}),
28
+ }),
29
+ );
30
+
31
+ const watchTerminalMetadata: T3TerminalApplicationService["watchTerminalMetadata"] = () =>
32
+ rpc.subscribe(WS_METHODS.subscribeTerminalMetadata, (client) =>
33
+ client[WS_METHODS.subscribeTerminalMetadata]({}),
34
+ );
35
+
36
+ const getTerminalMetadataSnapshot = (): Effect.Effect<ReadonlyArray<TerminalSummary>, RpcError> =>
37
+ Effect.gen(function* () {
38
+ const item = yield* Stream.runHead(
39
+ rpc.subscribe(WS_METHODS.subscribeTerminalMetadata, (client) =>
40
+ client[WS_METHODS.subscribeTerminalMetadata]({}),
41
+ ),
42
+ );
43
+ const value = Option.getOrUndefined(item);
44
+ if (value === undefined || value.type !== "snapshot") {
45
+ return yield* Effect.fail(
46
+ new RpcError({
47
+ message: "server did not return terminal metadata snapshot",
48
+ method: WS_METHODS.subscribeTerminalMetadata,
49
+ }),
50
+ );
51
+ }
52
+ return value.terminals;
53
+ });
54
+
55
+ const listTerminals = Effect.fn("T3ApplicationLive.listTerminals")(function* (threadId: string) {
56
+ const snapshot = yield* orchestration.getShellSnapshot();
57
+ if (snapshot.threads.find((candidate) => candidate.id === threadId) === undefined) {
58
+ return yield* Effect.fail(
59
+ new ThreadLookupError({
60
+ message: `thread not found: ${threadId}`,
61
+ threadId,
62
+ }),
63
+ );
64
+ }
65
+ const terminals = yield* getTerminalMetadataSnapshot();
66
+ return terminals.filter((terminal) => terminal.threadId === threadId);
67
+ });
68
+
69
+ const getTerminal = Effect.fn("T3ApplicationLive.getTerminal")(function* (terminal: TerminalRef) {
70
+ const snapshot = yield* orchestration.getShellSnapshot();
71
+ if (snapshot.threads.find((candidate) => candidate.id === terminal.threadId) === undefined) {
72
+ return yield* Effect.fail(
73
+ new ThreadLookupError({
74
+ message: `thread not found: ${terminal.threadId}`,
75
+ threadId: terminal.threadId,
76
+ }),
77
+ );
78
+ }
79
+ const terminals = yield* getTerminalMetadataSnapshot();
80
+ const match = terminals.find(
81
+ (candidate) =>
82
+ candidate.threadId === terminal.threadId && candidate.terminalId === terminal.terminalId,
83
+ );
84
+ if (match !== undefined) {
85
+ return match;
86
+ }
87
+ return yield* Effect.fail(
88
+ new TerminalLookupError({
89
+ message: `terminal not found: ${terminal.terminalId} in thread ${terminal.threadId}`,
90
+ threadId: terminal.threadId,
91
+ terminalId: terminal.terminalId,
92
+ }),
93
+ );
94
+ });
95
+
96
+ const createTerminal = Effect.fn("T3ApplicationLive.createTerminal")(function* (
97
+ input: CreateTerminalInput,
98
+ ) {
99
+ const snapshot = yield* orchestration.getShellSnapshot();
100
+ const thread = snapshot.threads.find((candidate) => candidate.id === input.threadId);
101
+ if (thread === undefined) {
102
+ return yield* Effect.fail(
103
+ new ThreadLookupError({
104
+ message: `thread not found: ${input.threadId}`,
105
+ threadId: input.threadId,
106
+ }),
107
+ );
108
+ }
109
+ const project = findProjectById(snapshot, thread.projectId);
110
+ if (project === null) {
111
+ return yield* Effect.fail(
112
+ new ProjectLookupError({
113
+ message: `project not found for thread: ${input.threadId}`,
114
+ ref: thread.projectId,
115
+ }),
116
+ );
117
+ }
118
+
119
+ const terminalId = input.terminalId ?? `t3cli-${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`;
120
+ const cwd = thread.worktreePath ?? project.workspaceRoot;
121
+ const opened = yield* rpc.run(WS_METHODS.terminalOpen, (client) =>
122
+ client[WS_METHODS.terminalOpen]({
123
+ threadId: thread.id,
124
+ terminalId,
125
+ cwd,
126
+ worktreePath: thread.worktreePath ?? null,
127
+ }),
128
+ );
129
+
130
+ if (input.command !== undefined) {
131
+ yield* rpc.run(WS_METHODS.terminalWrite, (client) =>
132
+ client[WS_METHODS.terminalWrite]({
133
+ threadId: thread.id,
134
+ terminalId,
135
+ data: `${input.command}\r`,
136
+ }),
137
+ );
138
+ const refreshed = yield* Stream.runHead(
139
+ attachTerminal({
140
+ terminal: {
141
+ threadId: opened.threadId,
142
+ terminalId: opened.terminalId,
143
+ cwd: opened.cwd,
144
+ worktreePath: opened.worktreePath,
145
+ },
146
+ }),
147
+ );
148
+ const event = Option.getOrUndefined(refreshed);
149
+ if (event !== undefined && (event.type === "snapshot" || event.type === "restarted")) {
150
+ return event.snapshot;
151
+ }
152
+ }
153
+
154
+ return opened;
155
+ });
156
+
157
+ const writeTerminal = Effect.fn("T3ApplicationLive.writeTerminal")(function* (input: {
158
+ readonly terminal: TerminalRef;
159
+ readonly data: string;
160
+ }) {
161
+ yield* rpc.run(WS_METHODS.terminalWrite, (client) =>
162
+ client[WS_METHODS.terminalWrite]({
163
+ threadId: input.terminal.threadId,
164
+ terminalId: input.terminal.terminalId,
165
+ data: input.data,
166
+ }),
167
+ );
168
+ });
169
+
170
+ const resizeTerminal = Effect.fn("T3ApplicationLive.resizeTerminal")(function* (input: {
171
+ readonly terminal: TerminalRef;
172
+ readonly cols: number;
173
+ readonly rows: number;
174
+ }) {
175
+ yield* rpc.run(WS_METHODS.terminalResize, (client) =>
176
+ client[WS_METHODS.terminalResize]({
177
+ threadId: input.terminal.threadId,
178
+ terminalId: input.terminal.terminalId,
179
+ cols: input.cols,
180
+ rows: input.rows,
181
+ }),
182
+ );
183
+ });
184
+
185
+ const destroyTerminal = Effect.fn("T3ApplicationLive.destroyTerminal")(function* (
186
+ terminal: TerminalRef,
187
+ ) {
188
+ yield* rpc.run(WS_METHODS.terminalClose, (client) =>
189
+ client[WS_METHODS.terminalClose]({
190
+ threadId: terminal.threadId,
191
+ terminalId: terminal.terminalId,
192
+ deleteHistory: true,
193
+ }),
194
+ );
195
+ });
196
+
197
+ return {
198
+ attachTerminal,
199
+ createTerminal,
200
+ destroyTerminal,
201
+ getTerminal,
202
+ listTerminals,
203
+ resizeTerminal,
204
+ watchTerminalMetadata,
205
+ writeTerminal,
206
+ } satisfies T3TerminalApplicationService;
207
+ });
@@ -0,0 +1,43 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import * as NodeServices from "@effect/platform-node/NodeServices";
5
+ import { assert, describe, it } from "@effect/vitest";
6
+
7
+ import { makeThreadInterruptCommand, makeThreadUnarchiveCommand } from "./thread-commands.ts";
8
+
9
+ describe("thread command builders", () => {
10
+ it.layer(NodeServices.layer)("thread command builders", (t) => {
11
+ t.effect("makeThreadInterruptCommand includes turnId when provided", () =>
12
+ Effect.gen(function* () {
13
+ const command = yield* makeThreadInterruptCommand({
14
+ threadId: "thread-1",
15
+ turnId: "turn-1",
16
+ });
17
+ assert.equal(command.type, "thread.turn.interrupt");
18
+ assert.equal(command.threadId, "thread-1");
19
+ assert.equal(command.turnId, "turn-1");
20
+ assert.equal(typeof command.createdAt, "string");
21
+ }),
22
+ );
23
+
24
+ t.effect("makeThreadInterruptCommand omits turnId when not provided", () =>
25
+ Effect.gen(function* () {
26
+ const command = yield* makeThreadInterruptCommand({
27
+ threadId: "thread-1",
28
+ });
29
+ assert.equal(command.type, "thread.turn.interrupt");
30
+ assert.equal(command.threadId, "thread-1");
31
+ assert.equal("turnId" in command, false);
32
+ }),
33
+ );
34
+
35
+ t.effect("makeThreadUnarchiveCommand builds thread.unarchive command", () =>
36
+ Effect.gen(function* () {
37
+ const command = yield* makeThreadUnarchiveCommand("thread-1");
38
+ assert.equal(command.type, "thread.unarchive");
39
+ assert.equal(command.threadId, "thread-1");
40
+ }),
41
+ );
42
+ });
43
+ });
@@ -6,6 +6,7 @@ import {
6
6
  CommandId,
7
7
  MessageId,
8
8
  ThreadId,
9
+ TurnId,
9
10
  type ClientOrchestrationCommand,
10
11
  type ModelSelection,
11
12
  type OrchestrationProjectShell,
@@ -101,6 +102,63 @@ export const makeThreadArchiveCommand = Effect.fn("makeThreadArchiveCommand")(fu
101
102
  } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.archive" }>;
102
103
  });
103
104
 
105
+ export const makeThreadUnarchiveCommand = Effect.fn("makeThreadUnarchiveCommand")(function* (
106
+ threadId: string,
107
+ ) {
108
+ const crypto = yield* Crypto.Crypto;
109
+ return {
110
+ type: "thread.unarchive",
111
+ commandId: CommandId.make(
112
+ `t3cli:thread-unarchive:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
113
+ ),
114
+ threadId: ThreadId.make(threadId),
115
+ } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.unarchive" }>;
116
+ });
117
+
118
+ export const makeThreadSessionStopCommand = Effect.fn("makeThreadSessionStopCommand")(function* (
119
+ threadId: string,
120
+ ) {
121
+ const crypto = yield* Crypto.Crypto;
122
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
123
+ return {
124
+ type: "thread.session.stop",
125
+ commandId: CommandId.make(
126
+ `t3cli:thread-session-stop:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
127
+ ),
128
+ threadId: ThreadId.make(threadId),
129
+ createdAt,
130
+ } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.session.stop" }>;
131
+ });
132
+
133
+ export const makeThreadDeleteCommand = Effect.fn("makeThreadDeleteCommand")(function* (
134
+ threadId: string,
135
+ ) {
136
+ const crypto = yield* Crypto.Crypto;
137
+ return {
138
+ type: "thread.delete",
139
+ commandId: CommandId.make(
140
+ `t3cli:thread-delete:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
141
+ ),
142
+ threadId: ThreadId.make(threadId),
143
+ } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.delete" }>;
144
+ });
145
+
146
+ export const makeThreadInterruptCommand = Effect.fn("makeThreadInterruptCommand")(
147
+ function* (input: { readonly threadId: string; readonly turnId?: string }) {
148
+ const crypto = yield* Crypto.Crypto;
149
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
150
+ return {
151
+ type: "thread.turn.interrupt",
152
+ commandId: CommandId.make(
153
+ `t3cli:thread-interrupt:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
154
+ ),
155
+ threadId: ThreadId.make(input.threadId),
156
+ ...(input.turnId !== undefined ? { turnId: TurnId.make(input.turnId) } : {}),
157
+ createdAt,
158
+ } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.turn.interrupt" }>;
159
+ },
160
+ );
161
+
104
162
  export const makeThreadApprovalRespondCommand = Effect.fn("makeThreadApprovalRespondCommand")(
105
163
  function* (input: {
106
164
  readonly threadId: string;
@@ -142,3 +200,26 @@ export const makeThreadUserInputRespondCommand = Effect.fn("makeThreadUserInputR
142
200
  } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.user-input.respond" }>;
143
201
  },
144
202
  );
203
+
204
+ export const makeThreadMetaUpdateCommand = Effect.fn("makeThreadMetaUpdateCommand")(function* (
205
+ threadId: string,
206
+ input: {
207
+ readonly title?: string;
208
+ readonly modelSelection?: ModelSelection;
209
+ readonly branch?: string | null;
210
+ readonly worktreePath?: string | null;
211
+ },
212
+ ) {
213
+ const crypto = yield* Crypto.Crypto;
214
+ return {
215
+ type: "thread.meta.update",
216
+ commandId: CommandId.make(
217
+ `t3cli:thread-meta-update:${yield* crypto.randomUUIDv4.pipe(Effect.orDie)}`,
218
+ ),
219
+ threadId: ThreadId.make(threadId),
220
+ ...(input.title !== undefined ? { title: input.title } : {}),
221
+ ...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
222
+ ...(input.branch !== undefined ? { branch: input.branch } : {}),
223
+ ...(input.worktreePath !== undefined ? { worktreePath: input.worktreePath } : {}),
224
+ } satisfies Extract<ClientOrchestrationCommand, { readonly type: "thread.meta.update" }>;
225
+ });