t3code-cli 0.9.1 → 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 (126) hide show
  1. package/README.md +52 -14
  2. package/dist/application.js +2 -2
  3. package/dist/auth.js +1 -1
  4. package/dist/bin.js +1556 -341
  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 +1734 -756
  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 +14 -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 +9 -13
  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/messages.ts +3 -3
  111. package/src/cli/threads/send.ts +22 -2
  112. package/src/cli/threads/unarchive.ts +44 -0
  113. package/src/cli/threads/update.ts +150 -0
  114. package/src/domain/error.ts +20 -1
  115. package/src/domain/model-config.ts +4 -0
  116. package/src/domain/thread-lifecycle.test.ts +35 -0
  117. package/src/domain/thread-lifecycle.ts +29 -6
  118. package/src/node/index.ts +2 -0
  119. package/src/orchestration/index.ts +1 -0
  120. package/src/orchestration/layer.ts +33 -85
  121. package/src/orchestration/service.ts +4 -0
  122. package/src/rpc/error.ts +10 -0
  123. package/src/rpc/index.ts +5 -0
  124. package/src/rpc/operation.ts +83 -0
  125. package/src/rpc/ws-group.ts +24 -0
  126. package/src/runtime/layer.ts +7 -2
@@ -0,0 +1,72 @@
1
+ import * as Crypto from "effect/Crypto";
2
+ import * as Effect from "effect/Effect";
3
+ import type { ModelSelection } from "#t3tools/contracts";
4
+
5
+ import { ModelSelectionError, ThreadEventError } from "../domain/error.ts";
6
+ import { T3Orchestration } from "../orchestration/service.ts";
7
+ import { mergeModelOptions, resolveUpdateModelSelection } from "./model-selection.ts";
8
+ import type { UpdateThreadInput } from "./service.ts";
9
+ import { makeThreadMetaUpdateCommand } from "./thread-commands.ts";
10
+
11
+ export function makeUpdateThread() {
12
+ return Effect.fn("T3ApplicationLive.updateThread")(function* (input: UpdateThreadInput) {
13
+ const orchestration = yield* T3Orchestration;
14
+ const crypto = yield* Crypto.Crypto;
15
+ const hasProvider = input.provider !== undefined && input.provider.length > 0;
16
+ const hasModel = input.model !== undefined && input.model.length > 0;
17
+ const options = input.options;
18
+ const hasOptions = options !== undefined && options.length > 0;
19
+ const thread = yield* orchestration.getThreadSnapshot(input.threadId);
20
+ let modelSelection: ModelSelection | undefined;
21
+ if (hasProvider || hasModel) {
22
+ const snapshot = yield* orchestration.getShellSnapshot();
23
+ const project = snapshot.projects.find((entry) => entry.id === thread.projectId);
24
+ if (project === undefined) {
25
+ return yield* Effect.fail(
26
+ new ThreadEventError({
27
+ message: `project not found for thread: ${input.threadId}`,
28
+ }),
29
+ );
30
+ }
31
+ const serverConfig = yield* orchestration.getServerConfig();
32
+ if (hasProvider && thread.session !== null && thread.session.status !== "stopped") {
33
+ const sessionProviderId =
34
+ thread.session.providerInstanceId ?? thread.modelSelection.instanceId;
35
+ const sessionProvider = serverConfig.providers.find(
36
+ (provider) => provider.instanceId === sessionProviderId,
37
+ );
38
+ const targetProvider = serverConfig.providers.find(
39
+ (provider) => provider.instanceId === input.provider,
40
+ );
41
+ if (
42
+ sessionProvider !== undefined &&
43
+ targetProvider !== undefined &&
44
+ targetProvider.driver !== sessionProvider.driver
45
+ ) {
46
+ return yield* Effect.fail(
47
+ new ModelSelectionError({
48
+ message: `thread ${input.threadId} is bound to ${sessionProvider.driver} provider ${sessionProviderId}; cannot update provider to ${targetProvider.driver} provider ${input.provider}`,
49
+ }),
50
+ );
51
+ }
52
+ }
53
+ modelSelection = yield* resolveUpdateModelSelection({
54
+ current: thread.modelSelection,
55
+ ...(hasProvider ? { provider: input.provider } : {}),
56
+ ...(hasModel ? { model: input.model } : {}),
57
+ ...(hasOptions ? { options } : {}),
58
+ project,
59
+ serverConfig,
60
+ });
61
+ } else if (hasOptions) {
62
+ modelSelection = mergeModelOptions(thread.modelSelection, options);
63
+ }
64
+ const command = yield* makeThreadMetaUpdateCommand(input.threadId, {
65
+ ...(input.title !== undefined ? { title: input.title } : {}),
66
+ ...(modelSelection !== undefined ? { modelSelection } : {}),
67
+ ...(input.branch !== undefined ? { branch: input.branch } : {}),
68
+ ...(input.worktreePath !== undefined ? { worktreePath: input.worktreePath } : {}),
69
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
70
+ return yield* orchestration.dispatch(command);
71
+ });
72
+ }
@@ -4,7 +4,7 @@ import * as Stream from "effect/Stream";
4
4
  import type { OrchestrationMessage, OrchestrationThread } from "#t3tools/contracts";
5
5
 
6
6
  import { ThreadSessionError } from "../domain/error.ts";
7
- import type { Orchestration } from "../orchestration/service.ts";
7
+ import { T3Orchestration } from "../orchestration/service.ts";
8
8
  import type { WaitEvent } from "./service.ts";
9
9
  import {
10
10
  applyThreadEvent,
@@ -15,60 +15,59 @@ import {
15
15
  threadStatus,
16
16
  } from "../domain/thread-lifecycle.ts";
17
17
 
18
- export function watchThread(input: {
19
- readonly orchestration: Orchestration;
20
- readonly threadId: string;
21
- }) {
18
+ export function watchThread(input: { readonly threadId: string }) {
22
19
  let current: OrchestrationThread | undefined;
23
20
  let currentMessages: Map<string, OrchestrationMessage> | undefined;
24
- return Stream.scoped(
25
- input.orchestration.watchThreadItems(input.threadId).pipe(
26
- Stream.flatMap((item) => {
27
- if (item.kind === "snapshot") {
28
- const messages = new Map<string, OrchestrationMessage>();
29
- for (const message of item.snapshot.thread.messages) {
30
- messages.set(messageKey(message), message);
31
- }
32
- current = item.snapshot.thread;
33
- currentMessages = messages;
21
+ return Stream.unwrap(
22
+ Effect.gen(function* () {
23
+ const orchestration = yield* T3Orchestration;
24
+ return Stream.scoped(
25
+ orchestration.watchThreadItems(input.threadId).pipe(
26
+ Stream.flatMap((item) => {
27
+ if (item.kind === "snapshot") {
28
+ const messages = new Map<string, OrchestrationMessage>();
29
+ for (const message of item.snapshot.thread.messages) {
30
+ messages.set(messageKey(message), message);
31
+ }
32
+ current = item.snapshot.thread;
33
+ currentMessages = messages;
34
34
 
35
- const events: Array<WaitEvent> = [
36
- { type: "thread", thread: current },
37
- { type: "status", status: threadStatus(current), threadId: current.id },
38
- ];
39
- if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
40
- events.push({ type: "done", thread: current });
41
- }
42
- return Stream.fromIterable(events);
43
- }
35
+ const events: Array<WaitEvent> = [
36
+ { type: "thread", thread: current },
37
+ { type: "status", status: threadStatus(current), threadId: current.id },
38
+ ];
39
+ if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
40
+ events.push({ type: "done", thread: current });
41
+ }
42
+ return Stream.fromIterable(events);
43
+ }
44
44
 
45
- if (current === undefined || currentMessages === undefined) {
46
- return Stream.fail(
47
- new ThreadSessionError({
48
- message: `thread stream event received before snapshot: ${input.threadId}`,
49
- threadId: input.threadId,
50
- }),
51
- );
52
- }
45
+ if (current === undefined || currentMessages === undefined) {
46
+ return Stream.fail(
47
+ new ThreadSessionError({
48
+ message: `thread stream event received before snapshot: ${input.threadId}`,
49
+ threadId: input.threadId,
50
+ }),
51
+ );
52
+ }
53
53
 
54
- current = applyThreadEvent(current, item.event, currentMessages);
55
- const message = messageFromEvent(item.event, currentMessages);
56
- const events: Array<WaitEvent> = message !== null ? [{ type: "message", message }] : [];
57
- events.push({ type: "status", status: threadStatus(current), threadId: current.id });
58
- if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
59
- events.push({ type: "done", thread: current });
60
- }
61
- return Stream.fromIterable(events);
62
- }),
63
- Stream.takeUntil((event) => event.type === "done"),
64
- ),
54
+ current = applyThreadEvent(current, item.event, currentMessages);
55
+ const message = messageFromEvent(item.event, currentMessages);
56
+ const events: Array<WaitEvent> = message !== null ? [{ type: "message", message }] : [];
57
+ events.push({ type: "status", status: threadStatus(current), threadId: current.id });
58
+ if (!isThreadActive(current) && isThreadCompleteEnough(current)) {
59
+ events.push({ type: "done", thread: current });
60
+ }
61
+ return Stream.fromIterable(events);
62
+ }),
63
+ Stream.takeUntil((event) => event.type === "done"),
64
+ ),
65
+ );
66
+ }),
65
67
  );
66
68
  }
67
69
 
68
- export function waitForThread(input: {
69
- readonly orchestration: Orchestration;
70
- readonly threadId: string;
71
- }) {
70
+ export function waitForThread(input: { readonly threadId: string }) {
72
71
  return watchThread(input).pipe(
73
72
  Stream.runLast,
74
73
  Effect.flatMap((event) => {
@@ -0,0 +1,166 @@
1
+ import "vite-plus/test/config";
2
+
3
+ import * as Effect from "effect/Effect";
4
+ import * as Layer from "effect/Layer";
5
+ import * as NodeServices from "@effect/platform-node/NodeServices";
6
+ import { assert, describe, it } from "@effect/vitest";
7
+ import { fromPartial } from "@total-typescript/shoehorn";
8
+ import type {
9
+ ClientOrchestrationCommand,
10
+ OrchestrationShellSnapshot,
11
+ OrchestrationThread,
12
+ OrchestrationThreadShell,
13
+ } from "#t3tools/contracts";
14
+
15
+ import { NodeEnvironmentLive } from "../environment/layer.ts";
16
+ import { T3Orchestration, type Orchestration } from "../orchestration/service.ts";
17
+ import { makeThreadApplication } from "./threads.ts";
18
+
19
+ function makeThread(
20
+ id: string,
21
+ projectId: string,
22
+ archivedAt: string | null,
23
+ ): OrchestrationThreadShell {
24
+ return fromPartial({
25
+ id,
26
+ projectId,
27
+ archivedAt,
28
+ title: `thread-${id}`,
29
+ updatedAt: "2026-01-01T00:00:00.000Z",
30
+ });
31
+ }
32
+
33
+ function makeSnapshot(threads: OrchestrationThreadShell[]): OrchestrationShellSnapshot {
34
+ return fromPartial({
35
+ projects: [{ id: "proj-1", workspaceRoot: "/workspace" }],
36
+ threads,
37
+ });
38
+ }
39
+
40
+ function makeOrchestrationLayer(input: {
41
+ readonly active?: OrchestrationShellSnapshot;
42
+ readonly archived?: OrchestrationShellSnapshot;
43
+ readonly threadSnapshot?: OrchestrationThread;
44
+ readonly onDispatch?: (command: ClientOrchestrationCommand) => void;
45
+ }) {
46
+ return Layer.succeed(
47
+ T3Orchestration,
48
+ fromPartial<Orchestration>({
49
+ getShellSnapshot: () => Effect.succeed(input.active ?? makeSnapshot([])),
50
+ getArchivedShellSnapshot: () => Effect.succeed(input.archived ?? makeSnapshot([])),
51
+ getThreadSnapshot: () => Effect.succeed(input.threadSnapshot ?? fromPartial({})),
52
+ dispatch: (command: ClientOrchestrationCommand) => {
53
+ input.onDispatch?.(command);
54
+ return Effect.succeed({ sequence: 42 });
55
+ },
56
+ }),
57
+ );
58
+ }
59
+
60
+ const testLayer = (orchestration: Layer.Layer<T3Orchestration>) =>
61
+ Layer.mergeAll(orchestration, NodeServices.layer, NodeEnvironmentLive);
62
+
63
+ describe("interruptThread", () => {
64
+ it.layer(NodeServices.layer)("interruptThread", (t) => {
65
+ t.effect("dispatches thread.turn.interrupt with activeTurnId from snapshot", () =>
66
+ Effect.gen(function* () {
67
+ let dispatched: ClientOrchestrationCommand | undefined;
68
+ const threadSnapshot = fromPartial<OrchestrationThread>({
69
+ id: "thread-1",
70
+ session: {
71
+ activeTurnId: "turn-1",
72
+ },
73
+ });
74
+ const app = yield* makeThreadApplication().pipe(
75
+ Effect.provide(
76
+ testLayer(
77
+ makeOrchestrationLayer({
78
+ threadSnapshot,
79
+ onDispatch: (command) => {
80
+ dispatched = command;
81
+ },
82
+ }),
83
+ ),
84
+ ),
85
+ );
86
+
87
+ const dispatch = yield* app.interruptThread("thread-1");
88
+ assert.equal(dispatch.sequence, 42);
89
+ assert.equal(dispatched?.type, "thread.turn.interrupt");
90
+ if (dispatched?.type === "thread.turn.interrupt") {
91
+ assert.equal(dispatched.threadId, "thread-1");
92
+ assert.equal(dispatched.turnId, "turn-1");
93
+ }
94
+ }),
95
+ );
96
+ });
97
+ });
98
+
99
+ describe("listThreads", () => {
100
+ it.layer(NodeServices.layer)("listThreads", (t) => {
101
+ t.effect("returns active threads by default", () =>
102
+ Effect.gen(function* () {
103
+ const active = makeSnapshot([
104
+ makeThread("active-1", "proj-1", null),
105
+ makeThread("active-2", "proj-1", null),
106
+ ]);
107
+ const archived = makeSnapshot([
108
+ makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
109
+ ]);
110
+ const app = yield* makeThreadApplication().pipe(
111
+ Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
112
+ );
113
+
114
+ const result = yield* app.listThreads("proj-1");
115
+ assert.deepEqual(
116
+ result.threads.map((thread) => thread.id),
117
+ ["active-1", "active-2"],
118
+ );
119
+ }),
120
+ );
121
+
122
+ t.effect("returns archived threads when include is archived", () =>
123
+ Effect.gen(function* () {
124
+ const active = makeSnapshot([makeThread("active-1", "proj-1", null)]);
125
+ const archived = makeSnapshot([
126
+ makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
127
+ makeThread("archived-2", "proj-1", "2026-01-02T00:00:00.000Z"),
128
+ ]);
129
+ const app = yield* makeThreadApplication().pipe(
130
+ Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
131
+ );
132
+
133
+ const result = yield* app.listThreads("proj-1", { include: "archived" });
134
+ assert.deepEqual(
135
+ result.threads.map((thread) => thread.id),
136
+ ["archived-1", "archived-2"],
137
+ );
138
+ assert.isTrue(result.threads.every((thread) => thread.archivedAt !== null));
139
+ }),
140
+ );
141
+
142
+ t.effect("returns merged threads without duplicates when include is all", () =>
143
+ Effect.gen(function* () {
144
+ const active = makeSnapshot([
145
+ makeThread("active-1", "proj-1", null),
146
+ makeThread("shared", "proj-1", null),
147
+ ]);
148
+ const archived = makeSnapshot([
149
+ makeThread("archived-1", "proj-1", "2026-01-01T00:00:00.000Z"),
150
+ makeThread("shared", "proj-1", "2026-01-02T00:00:00.000Z"),
151
+ ]);
152
+ const app = yield* makeThreadApplication().pipe(
153
+ Effect.provide(testLayer(makeOrchestrationLayer({ active, archived }))),
154
+ );
155
+
156
+ const result = yield* app.listThreads("proj-1", { include: "all" });
157
+ assert.deepEqual(
158
+ result.threads.map((thread) => thread.id).toSorted(),
159
+ ["active-1", "archived-1", "shared"].toSorted(),
160
+ );
161
+ const shared = result.threads.find((thread) => thread.id === "shared");
162
+ assert.equal(shared?.archivedAt, "2026-01-02T00:00:00.000Z");
163
+ }),
164
+ );
165
+ });
166
+ });
@@ -1,25 +1,36 @@
1
1
  import * as Crypto from "effect/Crypto";
2
2
  import * as Effect from "effect/Effect";
3
3
  import * as Path from "effect/Path";
4
-
4
+ import * as Stream from "effect/Stream";
5
5
  import { Environment } from "../environment/service.ts";
6
6
  import { T3Orchestration } from "../orchestration/service.ts";
7
- import { ProjectLookupError, ThreadSessionError } from "../domain/error.ts";
7
+ import { ProjectLookupError, ThreadLookupError, ThreadSessionError } from "../domain/error.ts";
8
8
  import { resolveProjectScope } from "../domain/helpers.ts";
9
- import { type StartThreadInput } from "./service.ts";
10
- import type { SendThreadInput, CallbackThreadInput } from "./service.ts";
9
+ import { type ListThreadsInclude, type StartThreadInput } from "./service.ts";
10
+ import type { CallbackThreadInput, SendThreadInput } from "./service.ts";
11
+ import type { T3ThreadApplicationService } from "./service.ts";
12
+ import type { OrchestrationThreadShell } from "#t3tools/contracts";
11
13
  import { mergeModelOptions } from "./model-selection.ts";
12
14
  import { derivePendingApprovals, derivePendingUserInputs } from "../domain/thread-activities.ts";
13
- import { threadStatus, type ThreadLifecycleStatus } from "../domain/thread-lifecycle.ts";
15
+ import {
16
+ sessionNeedsStopBeforeDelete,
17
+ threadStatus,
18
+ type ThreadLifecycleStatus,
19
+ } from "../domain/thread-lifecycle.ts";
14
20
  import type { OrchestrationThread } from "#t3tools/contracts";
15
21
  import type { ProviderApprovalDecision, ProviderUserInputAnswers } from "#t3tools/contracts";
16
22
  import {
17
23
  makeThreadApprovalRespondCommand,
18
24
  makeThreadArchiveCommand,
25
+ makeThreadDeleteCommand,
26
+ makeThreadInterruptCommand,
27
+ makeThreadSessionStopCommand,
19
28
  makeThreadStartCommands,
20
29
  makeThreadTurnContinueCommand,
30
+ makeThreadUnarchiveCommand,
21
31
  makeThreadUserInputRespondCommand,
22
32
  } from "./thread-commands.ts";
33
+ import { makeUpdateThread } from "./thread-update.ts";
23
34
  import {
24
35
  waitForThread as waitForThreadUntilComplete,
25
36
  watchThread as watchThreadEvents,
@@ -31,8 +42,24 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
31
42
  const crypto = yield* Crypto.Crypto;
32
43
  const path = yield* Path.Path;
33
44
  const environment = yield* Environment;
34
- const listThreads = Effect.fn("T3ApplicationLive.listThreads")(function* (projectRef: string) {
35
- const snapshot = yield* orchestration.getShellSnapshot();
45
+ const awaitShellSequence = (sequence: number) =>
46
+ waitForShellSequence({ sequence }).pipe(Effect.provideService(T3Orchestration, orchestration));
47
+ const awaitThreadCompletion = (threadId: string) =>
48
+ waitForThreadUntilComplete({ threadId }).pipe(
49
+ Effect.provideService(T3Orchestration, orchestration),
50
+ );
51
+ const streamThreadEvents = (threadId: string) =>
52
+ watchThreadEvents({ threadId }).pipe(Stream.provideService(T3Orchestration, orchestration));
53
+ const listThreads = Effect.fn("T3ApplicationLive.listThreads")(function* (
54
+ projectRef: string,
55
+ options?: {
56
+ readonly include?: ListThreadsInclude;
57
+ },
58
+ ) {
59
+ const include = options?.include ?? "active";
60
+ const snapshot = yield* loadThreadsSnapshot(include).pipe(
61
+ Effect.provideService(T3Orchestration, orchestration),
62
+ );
36
63
  const scope = yield* resolveProjectScope(snapshot, {
37
64
  ref: projectRef,
38
65
  }).pipe(Effect.provideService(Path.Path, path));
@@ -61,6 +88,55 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
61
88
  );
62
89
  return yield* orchestration.dispatch(command);
63
90
  });
91
+ const unarchiveThread = Effect.fn("T3ApplicationLive.unarchiveThread")(function* (
92
+ threadId: string,
93
+ ) {
94
+ const command = yield* makeThreadUnarchiveCommand(threadId).pipe(
95
+ Effect.provideService(Crypto.Crypto, crypto),
96
+ );
97
+ return yield* orchestration.dispatch(command);
98
+ });
99
+ const interruptThread = Effect.fn("T3ApplicationLive.interruptThread")(function* (
100
+ threadId: string,
101
+ ) {
102
+ const snapshot = yield* orchestration.getThreadSnapshot(threadId);
103
+ const activeTurnId = snapshot.session?.activeTurnId ?? undefined;
104
+ const command = yield* makeThreadInterruptCommand({
105
+ threadId,
106
+ ...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
107
+ }).pipe(Effect.provideService(Crypto.Crypto, crypto));
108
+ return yield* orchestration.dispatch(command);
109
+ });
110
+ const deleteThread = Effect.fn("T3ApplicationLive.deleteThread")(function* (threadId: string) {
111
+ const snapshot = yield* loadThreadsSnapshot("all").pipe(
112
+ Effect.provideService(T3Orchestration, orchestration),
113
+ );
114
+ const thread = snapshot.threads.find((item) => item.id === threadId);
115
+ if (thread === undefined) {
116
+ return yield* Effect.fail(
117
+ new ThreadLookupError({
118
+ message: `thread not found: ${threadId}`,
119
+ threadId,
120
+ }),
121
+ );
122
+ }
123
+ if (sessionNeedsStopBeforeDelete(thread.session)) {
124
+ const stopCommand = yield* makeThreadSessionStopCommand(threadId).pipe(
125
+ Effect.provideService(Crypto.Crypto, crypto),
126
+ );
127
+ yield* orchestration.dispatch(stopCommand);
128
+ }
129
+ const command = yield* makeThreadDeleteCommand(threadId).pipe(
130
+ Effect.provideService(Crypto.Crypto, crypto),
131
+ );
132
+ const dispatch = yield* orchestration.dispatch(command);
133
+ return { threadId, dispatch };
134
+ });
135
+ const updateThread: T3ThreadApplicationService["updateThread"] = (input) =>
136
+ makeUpdateThread()(input).pipe(
137
+ Effect.provideService(T3Orchestration, orchestration),
138
+ Effect.provideService(Crypto.Crypto, crypto),
139
+ );
64
140
  const startThread = Effect.fn("T3ApplicationLive.startThread")(function* (
65
141
  startInput: StartThreadInput,
66
142
  policy?: {
@@ -96,20 +172,14 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
96
172
  serverConfig,
97
173
  }).pipe(Effect.provideService(Crypto.Crypto, crypto));
98
174
  const createDispatch = yield* orchestration.dispatch(commands.createCommand);
99
- yield* waitForShellSequence({
100
- orchestration,
101
- sequence: createDispatch.sequence,
102
- });
175
+ yield* awaitShellSequence(createDispatch.sequence);
103
176
  const dispatch = yield* orchestration.dispatch(commands.turnCommand);
104
177
  const threadId = commands.threadId;
105
178
  const until = policy?.until ?? "dispatch";
106
179
  if (until === "dispatch") {
107
180
  return { dispatch, project: scope.project, threadId };
108
181
  }
109
- yield* waitForShellSequence({
110
- orchestration,
111
- sequence: dispatch.sequence,
112
- });
182
+ yield* awaitShellSequence(dispatch.sequence);
113
183
  if (until === "visible") {
114
184
  const thread = yield* Effect.scoped(
115
185
  Effect.gen(function* () {
@@ -119,10 +189,7 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
119
189
  );
120
190
  return { dispatch, project: scope.project, threadId, thread };
121
191
  }
122
- const thread = yield* waitForThreadUntilComplete({
123
- orchestration,
124
- threadId,
125
- });
192
+ const thread = yield* awaitThreadCompletion(threadId);
126
193
  yield* failIfThreadError(thread);
127
194
  return { dispatch, project: scope.project, threadId, thread };
128
195
  });
@@ -148,10 +215,7 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
148
215
  if (until === "dispatch") {
149
216
  return { dispatch, threadId: input.threadId };
150
217
  }
151
- yield* waitForShellSequence({
152
- orchestration,
153
- sequence: dispatch.sequence,
154
- });
218
+ yield* awaitShellSequence(dispatch.sequence);
155
219
  if (until === "visible") {
156
220
  const thread = yield* Effect.scoped(
157
221
  Effect.gen(function* () {
@@ -161,33 +225,20 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
161
225
  );
162
226
  return { dispatch, threadId: input.threadId, thread };
163
227
  }
164
- const thread = yield* waitForThreadUntilComplete({
165
- orchestration,
166
- threadId: input.threadId,
167
- });
228
+ const thread = yield* awaitThreadCompletion(input.threadId);
168
229
  yield* failIfThreadError(thread);
169
230
  return { dispatch, threadId: input.threadId, thread };
170
231
  });
171
- const watchThread = (threadId: string) =>
172
- watchThreadEvents({
173
- orchestration,
174
- threadId,
175
- });
232
+ const watchThread = (threadId: string) => streamThreadEvents(threadId);
176
233
  const waitForThread = Effect.fn("T3ApplicationLive.waitForThread")(function* (threadId: string) {
177
- const thread = yield* waitForThreadUntilComplete({
178
- orchestration,
179
- threadId,
180
- });
234
+ const thread = yield* awaitThreadCompletion(threadId);
181
235
  yield* failIfThreadError(thread);
182
236
  return thread;
183
237
  });
184
238
  const callbackThread = Effect.fn("T3ApplicationLive.callbackThread")(function* (
185
239
  input: CallbackThreadInput,
186
240
  ) {
187
- yield* waitForThreadUntilComplete({
188
- orchestration,
189
- threadId: input.fromThreadId,
190
- });
241
+ yield* awaitThreadCompletion(input.fromThreadId);
191
242
  const result = yield* sendThread(
192
243
  { threadId: input.targetThreadId, message: input.prompt },
193
244
  { until: "dispatch" },
@@ -220,6 +271,10 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
220
271
  return {
221
272
  approveThread,
222
273
  archiveThread,
274
+ deleteThread,
275
+ interruptThread,
276
+ updateThread,
277
+ unarchiveThread,
223
278
  listThreads,
224
279
  getThreadMessages,
225
280
  respondToThread,
@@ -229,9 +284,37 @@ export const makeThreadApplication = Effect.fn("makeThreadApplication")(function
229
284
  watchThread,
230
285
  waitForThread,
231
286
  callbackThread,
287
+ } satisfies T3ThreadApplicationService;
288
+ });
289
+
290
+ const loadThreadsSnapshot = Effect.fn("loadThreadsSnapshot")(function* (
291
+ include: ListThreadsInclude,
292
+ ) {
293
+ const orchestration = yield* T3Orchestration;
294
+ if (include === "active") {
295
+ return yield* orchestration.getShellSnapshot();
296
+ }
297
+ if (include === "archived") {
298
+ return yield* orchestration.getArchivedShellSnapshot();
299
+ }
300
+ const [activeSnapshot, archivedSnapshot] = yield* Effect.all([
301
+ orchestration.getShellSnapshot(),
302
+ orchestration.getArchivedShellSnapshot(),
303
+ ]);
304
+ return {
305
+ ...activeSnapshot,
306
+ threads: dedupeThreadsById([...activeSnapshot.threads, ...archivedSnapshot.threads]),
232
307
  };
233
308
  });
234
309
 
310
+ function dedupeThreadsById(threads: ReadonlyArray<OrchestrationThreadShell>) {
311
+ const byId = new Map<string, OrchestrationThreadShell>();
312
+ for (const thread of threads) {
313
+ byId.set(thread.id, thread);
314
+ }
315
+ return [...byId.values()];
316
+ }
317
+
235
318
  export type ThreadShow = {
236
319
  readonly id: string;
237
320
  readonly projectId: string;
package/src/bin.ts CHANGED
@@ -9,6 +9,7 @@ import { createCliCommand } from "./cli/app.ts";
9
9
  import { NodeEnvironmentLive } from "./environment/layer.ts";
10
10
  import { T3InputLive } from "./cli/input/layer.ts";
11
11
  import { T3OutputLive } from "./cli/output/layer.ts";
12
+ import { NodeTerminalIoLive } from "./cli/terminal/io-node-layer.ts";
12
13
  import { T3Output } from "./cli/output/service.ts";
13
14
  import { AppLayer } from "./runtime/layer.ts";
14
15
  import { T3VersionBundledLive, T3VersionPackageJsonLive } from "./version/layer.ts";
@@ -27,6 +28,7 @@ const CliLayer = Layer.mergeAll(
27
28
  NodeEnvironmentLive,
28
29
  T3InputLive.pipe(Layer.provide(NodeServices.layer)),
29
30
  T3OutputLive.pipe(Layer.provide(NodeServices.layer)),
31
+ NodeTerminalIoLive,
30
32
  VersionLive.pipe(Layer.provide(NodeServices.layer)),
31
33
  );
32
34
 
package/src/cli/app.ts CHANGED
@@ -1,17 +1,31 @@
1
1
  import { Command } from "effect/unstable/cli";
2
2
 
3
3
  import { createAuthCommand } from "./auth.ts";
4
+ import { createTerminalCommandGroup } from "./terminal.ts";
4
5
  import { createModelCommand } from "./model.ts";
5
6
  import { createProjectCommand } from "./project.ts";
6
7
  import { createThreadCommand } from "./thread.ts";
8
+ import { listThreadsCommand } from "./threads/list.ts";
9
+ import { sendThreadCommand } from "./threads/send.ts";
10
+ import { showThreadCommand } from "./threads/show.ts";
11
+ import { startThreadCommand } from "./threads/start.ts";
12
+ import { getThreadTranscriptCommand } from "./threads/messages.ts";
13
+ import { waitForThreadCommand } from "./threads/wait.ts";
7
14
 
8
15
  export function createCliCommand() {
9
16
  return Command.make("t3cli").pipe(
10
17
  Command.withDescription("non-interactive cli for running t3code server"),
11
18
  Command.withSubcommands([
12
19
  createAuthCommand(),
20
+ listThreadsCommand,
13
21
  createModelCommand(),
14
22
  createProjectCommand(),
23
+ createTerminalCommandGroup(),
24
+ startThreadCommand,
25
+ sendThreadCommand,
26
+ showThreadCommand,
27
+ getThreadTranscriptCommand,
28
+ waitForThreadCommand,
15
29
  createThreadCommand(),
16
30
  ]),
17
31
  );