t3code-cli 0.14.1 → 0.15.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.
- package/README.md +37 -1
- package/dist/application.d.ts +2 -2
- package/dist/application.js +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/bin.js +1305 -424
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/client-runtime/authorization.d.ts +1 -1
- package/dist/client-runtime/authorization.js +1 -1
- package/dist/client-runtime/connection.d.ts +1 -1
- package/dist/client-runtime/connection.js +1 -1
- package/dist/client-runtime/environment.d.ts +7 -1
- package/dist/client-runtime/environment.js +1 -1
- package/dist/client-runtime/errors.js +1 -1
- package/dist/client-runtime/operations/projects.d.ts +2 -2
- package/dist/client-runtime/operations/projects.js +2 -2
- package/dist/client-runtime/operations.d.ts +2 -2
- package/dist/client-runtime/operations.js +2 -2
- package/dist/client-runtime/platform.d.ts +1 -1
- package/dist/client-runtime/platform.js +1 -1
- package/dist/client-runtime/relay.d.ts +13 -2
- package/dist/client-runtime/relay.js +12 -2
- package/dist/client-runtime/rpc.d.ts +2 -2
- package/dist/client-runtime/rpc.js +2 -2
- package/dist/client-runtime/state/assets.d.ts +17 -11
- package/dist/client-runtime/state/assets.js +1 -1
- package/dist/client-runtime/state/auth.d.ts +1 -1
- package/dist/client-runtime/state/auth.js +1 -1
- package/dist/client-runtime/state/connections.js +1 -1
- package/dist/client-runtime/state/entities.d.ts +1 -1
- package/dist/client-runtime/state/entities.js +1 -1
- package/dist/client-runtime/state/filesystem.d.ts +1 -1
- package/dist/client-runtime/state/filesystem.js +1 -1
- package/dist/client-runtime/state/git.d.ts +6 -5
- package/dist/client-runtime/state/git.js +1 -1
- package/dist/client-runtime/state/models.d.ts +1 -1
- package/dist/client-runtime/state/models.js +1 -1
- package/dist/client-runtime/state/orchestration.d.ts +42 -2
- package/dist/client-runtime/state/orchestration.js +13 -1
- package/dist/client-runtime/state/presentation.d.ts +15 -3
- package/dist/client-runtime/state/presentation.js +1 -1
- package/dist/client-runtime/state/preview.d.ts +58 -48
- package/dist/client-runtime/state/preview.js +3 -2
- package/dist/client-runtime/state/project-grouping.d.ts +26 -2
- package/dist/client-runtime/state/project-grouping.js +83 -3
- package/dist/client-runtime/state/projects.d.ts +24 -18
- package/dist/client-runtime/state/projects.js +1 -1
- package/dist/client-runtime/state/pull-requests.d.ts +478 -0
- package/dist/client-runtime/state/pull-requests.js +172 -0
- package/dist/client-runtime/state/relay.d.ts +1 -1
- package/dist/client-runtime/state/relay.js +1 -1
- package/dist/client-runtime/state/review.d.ts +16 -1
- package/dist/client-runtime/state/review.js +27 -6
- package/dist/client-runtime/state/runtime.d.ts +2 -2
- package/dist/client-runtime/state/runtime.js +2 -2
- package/dist/client-runtime/state/server.d.ts +311 -172
- package/dist/client-runtime/state/server.js +192 -16
- package/dist/client-runtime/state/session.d.ts +77 -7
- package/dist/client-runtime/state/session.js +32 -3
- package/dist/client-runtime/state/shell.d.ts +47 -7
- package/dist/client-runtime/state/shell.js +1 -1
- package/dist/client-runtime/state/source-control.d.ts +2 -2
- package/dist/client-runtime/state/source-control.js +1 -1
- package/dist/client-runtime/state/subagentRuntime.d.ts +145 -0
- package/dist/client-runtime/state/subagentRuntime.js +521 -0
- package/dist/client-runtime/state/terminal.d.ts +17 -17
- package/dist/client-runtime/state/terminal.js +1 -1
- package/dist/client-runtime/state/thread-search.d.ts +23 -0
- package/dist/client-runtime/state/thread-search.js +38 -0
- package/dist/client-runtime/state/thread-settled.d.ts +32 -4
- package/dist/client-runtime/state/thread-settled.js +2 -157
- package/dist/client-runtime/state/thread-sort.d.ts +59 -2
- package/dist/client-runtime/state/thread-sort.js +128 -2
- package/dist/client-runtime/state/threads.d.ts +115 -3
- package/dist/client-runtime/state/threads.js +309 -28
- package/dist/client-runtime/state/vcs.d.ts +14 -14
- package/dist/client-runtime/state/vcs.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/connection.d.ts +1 -1
- package/dist/connection.js +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +1 -1
- package/dist/orchestration.d.ts +155 -6
- package/dist/orchestration.js +2 -2
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +1 -1
- package/dist/rpc.d.ts +887 -360
- package/dist/rpc.js +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/shared/DrainableWorker.js +1 -1
- package/dist/shared/KeyedCoalescingWorker.js +1 -1
- package/dist/shared/Net.d.ts +6 -0
- package/dist/shared/Net.js +2 -1
- package/dist/shared/advertisedEndpoint.d.ts +1 -1
- package/dist/shared/advertisedEndpoint.js +1 -1
- package/dist/shared/agentAwareness.d.ts +1 -1
- package/dist/shared/backgroundActivitySettings.d.ts +1 -1
- package/dist/shared/composerInlineTokens.js +1 -1
- package/dist/shared/connectAuth.d.ts +23 -4
- package/dist/shared/connectAuth.js +40 -7
- package/dist/shared/devHome.js +1 -1
- package/dist/shared/dpop.js +1 -1
- package/dist/shared/git.d.ts +1 -1
- package/dist/shared/httpObservability.js +1 -1
- package/dist/shared/httpReadiness.js +1 -1
- package/dist/shared/keybindings.d.ts +2 -2
- package/dist/shared/keybindings.js +15 -1
- package/dist/shared/model.d.ts +1 -1
- package/dist/shared/oauthScope.js +1 -1
- package/dist/shared/observability.d.ts +8 -1
- package/dist/shared/observability.js +49 -5
- package/dist/shared/path.js +1 -1
- package/dist/shared/preview.js +1 -1
- package/dist/shared/previewViewport.d.ts +1 -1
- package/dist/shared/previewViewport.js +1 -1
- package/dist/shared/projectFavicon.d.ts +2 -1
- package/dist/shared/projectFavicon.js +13 -1
- package/dist/shared/projectScripts.d.ts +1 -1
- package/dist/shared/relayAuth.js +1 -1
- package/dist/shared/relayClient.d.ts +1 -1
- package/dist/shared/relayClient.js +1 -1
- package/dist/shared/relayJwt.js +1 -1
- package/dist/shared/relayTracing.js +1 -1
- package/dist/shared/relayUrl.js +1 -1
- package/dist/shared/remote.js +1 -1
- package/dist/shared/schemaYaml.js +3 -3
- package/dist/shared/semver.js +1 -1
- package/dist/shared/serverSettings.d.ts +1 -1
- package/dist/shared/serverSettings.js +1 -1
- package/dist/shared/shell.d.ts +6 -1
- package/dist/shared/shell.js +42 -4
- package/dist/shared/sourceControl.d.ts +1 -1
- package/dist/shared/t3ProjectFile.d.ts +9 -1
- package/dist/shared/t3ProjectFile.js +12 -2
- package/dist/shared/terminalLabels.d.ts +1 -1
- package/dist/shared/threadEnvMode.d.ts +29 -0
- package/dist/shared/threadEnvMode.js +24 -0
- package/dist/shared/toolActivity.d.ts +1 -1
- package/dist/shared/usageFormat.d.ts +34 -0
- package/dist/shared/usageFormat.js +176 -0
- package/dist/shared/usageMerge.d.ts +78 -0
- package/dist/shared/usageMerge.js +241 -0
- package/dist/shared.d.ts +8268 -1372
- package/dist/shared.js +22185 -19774
- package/dist/t3tools.d.ts +2 -2
- package/dist/t3tools.js +2 -2
- package/package.json +5 -5
- package/src/application/error.ts +2 -2
- package/src/application/index.ts +2 -0
- package/src/application/service.ts +43 -5
- package/src/application/thread-commands.ts +78 -0
- package/src/application/threads.ts +156 -17
- package/src/cli/app.ts +4 -0
- package/src/cli/ask-lifecycle.ts +331 -0
- package/src/cli/ask.ts +390 -0
- package/src/cli/error.ts +70 -0
- package/src/cli/format/thread.ts +49 -7
- package/src/cli/search.ts +48 -0
- package/src/cli/thread.ts +12 -0
- package/src/cli/threads/messages.ts +33 -3
- package/src/cli/threads/pin.ts +46 -0
- package/src/cli/threads/send.ts +5 -1
- package/src/cli/threads/settle.ts +46 -0
- package/src/cli/threads/snooze.ts +117 -0
- package/src/cli/threads/start.ts +6 -1
- package/src/cli/threads/unpin.ts +48 -0
- package/src/cli/threads/unsettle.ts +48 -0
- package/src/cli/threads/unsnooze.ts +48 -0
- package/src/config/persist/schema.ts +1 -1
- package/src/connection/prepared.ts +12 -2
- package/src/orchestration/error.ts +10 -0
- package/src/orchestration/index.ts +1 -0
- package/src/orchestration/layer.ts +78 -0
- package/src/orchestration/service.ts +16 -3
- package/src/rpc/error.ts +2 -0
- package/src/rpc/operation.ts +2 -0
- package/src/rpc/ws-group.ts +3 -0
- package/src/runtime/layer.ts +9 -4
package/src/cli/ask.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
6
|
+
import type { DispatchResult } from "@t3tools/contracts";
|
|
7
|
+
|
|
8
|
+
import { T3Application } from "../application/service.ts";
|
|
9
|
+
import { isThreadActive } from "../domain/thread-lifecycle.ts";
|
|
10
|
+
import { ThreadSessionError } from "../domain/error.ts";
|
|
11
|
+
import { loadT3CliEnv } from "../config/env/env.ts";
|
|
12
|
+
import {
|
|
13
|
+
archivePolicyChoices,
|
|
14
|
+
cleanupInterruptedAsk,
|
|
15
|
+
ensureAskTargetAvailable,
|
|
16
|
+
ensureTrailingNewline,
|
|
17
|
+
finalizeArchive,
|
|
18
|
+
inspectAskTurn,
|
|
19
|
+
resolveAskFormat,
|
|
20
|
+
selectAskAnswer,
|
|
21
|
+
waitForAskThread,
|
|
22
|
+
} from "./ask-lifecycle.ts";
|
|
23
|
+
import type { AskExecutionState } from "./ask-lifecycle.ts";
|
|
24
|
+
import { extraArgsConfig } from "./extra-args.ts";
|
|
25
|
+
import {
|
|
26
|
+
AskNoAnswerError,
|
|
27
|
+
AskProjectMismatchError,
|
|
28
|
+
AskThreadBusyError,
|
|
29
|
+
AskTimeoutError,
|
|
30
|
+
InvalidAskTimeoutError,
|
|
31
|
+
InvalidFlagCombinationError,
|
|
32
|
+
} from "./error.ts";
|
|
33
|
+
import { humanJsonNdjsonFormatChoices } from "./format/output.ts";
|
|
34
|
+
import { modelFlags, projectFlag, selfActionForceFlag, worktreeFlag } from "./flags.ts";
|
|
35
|
+
import { T3Input } from "./input/service.ts";
|
|
36
|
+
import { requireSelfActionConfirmation } from "./interaction/self-action.ts";
|
|
37
|
+
import { readInitialMessage } from "./message-input.ts";
|
|
38
|
+
import { buildModelOptions } from "./model-options.ts";
|
|
39
|
+
import { T3Output } from "./output/service.ts";
|
|
40
|
+
import { requireCommandProjectRef } from "./require.ts";
|
|
41
|
+
import { resolveWorktreePath } from "./scope/index.ts";
|
|
42
|
+
import { CliRuntime } from "./runtime/service.ts";
|
|
43
|
+
|
|
44
|
+
interface AskResponse {
|
|
45
|
+
readonly answer: string;
|
|
46
|
+
readonly threadId: string;
|
|
47
|
+
readonly turnId: string | null;
|
|
48
|
+
readonly created: boolean;
|
|
49
|
+
readonly dispatch: DispatchResult;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const askThreadFlag = Flag.string("thread").pipe(
|
|
53
|
+
Flag.withDescription("Existing thread id; T3CODE_THREAD_ID is not used"),
|
|
54
|
+
Flag.optional,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const archivePolicyFlag = Flag.choice("archive", archivePolicyChoices).pipe(
|
|
58
|
+
Flag.withDescription(
|
|
59
|
+
"Archive policy (default: on-success for created threads, never for existing threads)",
|
|
60
|
+
),
|
|
61
|
+
Flag.optional,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const timeoutFlag = Flag.string("timeout").pipe(
|
|
65
|
+
Flag.withDescription("Response timeout, such as 30s, 5m, or 1h"),
|
|
66
|
+
Flag.optional,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const askFormatFlag = Flag.choice("format", humanJsonNdjsonFormatChoices).pipe(
|
|
70
|
+
Flag.withDefault("human"),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
export const askCommand = Command.make(
|
|
74
|
+
"ask",
|
|
75
|
+
{
|
|
76
|
+
project: projectFlag,
|
|
77
|
+
thread: askThreadFlag,
|
|
78
|
+
force: selfActionForceFlag,
|
|
79
|
+
message: Argument.string("message").pipe(Argument.optional),
|
|
80
|
+
stdin: Flag.boolean("stdin"),
|
|
81
|
+
title: Flag.string("title").pipe(Flag.optional),
|
|
82
|
+
worktree: worktreeFlag,
|
|
83
|
+
provider: Flag.string("provider").pipe(Flag.optional),
|
|
84
|
+
model: Flag.string("model").pipe(Flag.optional),
|
|
85
|
+
...modelFlags,
|
|
86
|
+
archive: archivePolicyFlag,
|
|
87
|
+
timeout: timeoutFlag,
|
|
88
|
+
format: askFormatFlag,
|
|
89
|
+
...extraArgsConfig,
|
|
90
|
+
},
|
|
91
|
+
({
|
|
92
|
+
project,
|
|
93
|
+
thread,
|
|
94
|
+
force,
|
|
95
|
+
message,
|
|
96
|
+
stdin,
|
|
97
|
+
title,
|
|
98
|
+
worktree,
|
|
99
|
+
provider,
|
|
100
|
+
model,
|
|
101
|
+
option,
|
|
102
|
+
reasoningEffort,
|
|
103
|
+
effort,
|
|
104
|
+
fastMode,
|
|
105
|
+
thinking,
|
|
106
|
+
archive,
|
|
107
|
+
timeout,
|
|
108
|
+
format,
|
|
109
|
+
}) =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
const explicitThreadId = Option.getOrUndefined(thread);
|
|
112
|
+
const created = explicitThreadId === undefined;
|
|
113
|
+
const titleValue = Option.getOrUndefined(title);
|
|
114
|
+
const providerValue = Option.getOrUndefined(provider);
|
|
115
|
+
const modelValue = Option.getOrUndefined(model);
|
|
116
|
+
const createOnlyFlags = [
|
|
117
|
+
Option.isSome(title) ? "--title" : null,
|
|
118
|
+
Option.isSome(worktree) ? "--worktree" : null,
|
|
119
|
+
Option.isSome(provider) ? "--provider" : null,
|
|
120
|
+
Option.isSome(model) ? "--model" : null,
|
|
121
|
+
].filter((flag): flag is string => flag !== null);
|
|
122
|
+
|
|
123
|
+
if (!created && createOnlyFlags.length > 0) {
|
|
124
|
+
return yield* Effect.fail(
|
|
125
|
+
new InvalidFlagCombinationError({
|
|
126
|
+
message: `${createOnlyFlags.join(", ")} cannot be used with --thread`,
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (created && force) {
|
|
131
|
+
return yield* Effect.fail(
|
|
132
|
+
new InvalidFlagCombinationError({
|
|
133
|
+
message: "--force can only be used with --thread",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const timeoutValue = Option.getOrUndefined(timeout);
|
|
139
|
+
const timeoutDuration =
|
|
140
|
+
timeoutValue === undefined ? undefined : yield* parseAskTimeout(timeoutValue);
|
|
141
|
+
const archivePolicy = Option.getOrElse(archive, () => (created ? "on-success" : "never"));
|
|
142
|
+
const application = yield* T3Application;
|
|
143
|
+
const cliRuntime = yield* CliRuntime;
|
|
144
|
+
const t3CliEnv = yield* loadT3CliEnv;
|
|
145
|
+
const input = yield* T3Input;
|
|
146
|
+
const output = yield* T3Output;
|
|
147
|
+
const resolvedFormat = resolveAskFormat(format, cliRuntime, t3CliEnv);
|
|
148
|
+
const options = buildModelOptions({
|
|
149
|
+
option,
|
|
150
|
+
reasoningEffort,
|
|
151
|
+
effort,
|
|
152
|
+
fastMode,
|
|
153
|
+
thinking,
|
|
154
|
+
});
|
|
155
|
+
const state: AskExecutionState = {
|
|
156
|
+
archivePolicy,
|
|
157
|
+
threadId: explicitThreadId,
|
|
158
|
+
createdThread: false,
|
|
159
|
+
dispatched: false,
|
|
160
|
+
askTurnId: null,
|
|
161
|
+
archiveResult: undefined,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const response = Effect.gen(function* () {
|
|
165
|
+
const text = yield* readInitialMessage({
|
|
166
|
+
message: Option.getOrUndefined(message),
|
|
167
|
+
fromStdin: stdin,
|
|
168
|
+
readStdin: input.readStdin,
|
|
169
|
+
});
|
|
170
|
+
let dispatch: DispatchResult;
|
|
171
|
+
let askMessageId: string;
|
|
172
|
+
|
|
173
|
+
if (explicitThreadId === undefined) {
|
|
174
|
+
const projectRef = yield* requireCommandProjectRef({ project });
|
|
175
|
+
const resolvedProject = yield* application.resolveProject(projectRef);
|
|
176
|
+
const worktreePath = resolveWorktreePath({
|
|
177
|
+
value: Option.getOrUndefined(worktree),
|
|
178
|
+
scope: t3CliEnv.scope,
|
|
179
|
+
});
|
|
180
|
+
const result = yield* application.startThread(
|
|
181
|
+
{
|
|
182
|
+
message: text,
|
|
183
|
+
projectRef: resolvedProject.id,
|
|
184
|
+
...(titleValue !== undefined ? { title: titleValue } : {}),
|
|
185
|
+
...(worktreePath !== undefined ? { worktreePath } : {}),
|
|
186
|
+
...(providerValue !== undefined ? { provider: providerValue } : {}),
|
|
187
|
+
...(modelValue !== undefined ? { model: modelValue } : {}),
|
|
188
|
+
...(options.length > 0 ? { options } : {}),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
until: "dispatch",
|
|
192
|
+
onThreadCreated: (threadId) =>
|
|
193
|
+
Effect.sync(() => {
|
|
194
|
+
state.threadId = threadId;
|
|
195
|
+
state.createdThread = true;
|
|
196
|
+
}),
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
state.dispatched = true;
|
|
200
|
+
dispatch = result.dispatch;
|
|
201
|
+
askMessageId = result.messageId;
|
|
202
|
+
} else {
|
|
203
|
+
const summary = yield* application.getThreadSummary(explicitThreadId);
|
|
204
|
+
yield* ensureAskTargetAvailable(summary);
|
|
205
|
+
|
|
206
|
+
const projectRef = Option.getOrUndefined(project);
|
|
207
|
+
if (projectRef !== undefined) {
|
|
208
|
+
const resolvedProject = yield* application.resolveProject(projectRef);
|
|
209
|
+
if (summary.projectId !== resolvedProject.id) {
|
|
210
|
+
return yield* Effect.fail(
|
|
211
|
+
new AskProjectMismatchError({
|
|
212
|
+
message: `thread ${explicitThreadId} belongs to project ${summary.projectId}, not ${resolvedProject.id}`,
|
|
213
|
+
threadId: explicitThreadId,
|
|
214
|
+
projectId: resolvedProject.id,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
yield* requireSelfActionConfirmation({
|
|
221
|
+
threadId: explicitThreadId,
|
|
222
|
+
force,
|
|
223
|
+
cliRuntime,
|
|
224
|
+
t3CliEnv,
|
|
225
|
+
action: "ask",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const targetThread = (yield* application.getThreadMessages({
|
|
229
|
+
threadId: explicitThreadId,
|
|
230
|
+
})).thread;
|
|
231
|
+
if (isThreadActive(targetThread)) {
|
|
232
|
+
return yield* Effect.fail(
|
|
233
|
+
new AskThreadBusyError({
|
|
234
|
+
message: `thread is busy: ${explicitThreadId}`,
|
|
235
|
+
threadId: explicitThreadId,
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const result = yield* application.sendThread(
|
|
241
|
+
{
|
|
242
|
+
threadId: explicitThreadId,
|
|
243
|
+
message: text,
|
|
244
|
+
...(options.length > 0 ? { options } : {}),
|
|
245
|
+
},
|
|
246
|
+
{ until: "dispatch" },
|
|
247
|
+
);
|
|
248
|
+
state.dispatched = true;
|
|
249
|
+
dispatch = result.dispatch;
|
|
250
|
+
askMessageId = result.messageId;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const threadId = state.threadId;
|
|
254
|
+
if (threadId === undefined) {
|
|
255
|
+
return yield* Effect.fail(
|
|
256
|
+
new AskNoAnswerError({
|
|
257
|
+
message: "ask did not resolve a thread id",
|
|
258
|
+
threadId: "unknown",
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (resolvedFormat === "ndjson") {
|
|
264
|
+
yield* output.printNdjson({ type: "dispatch", sequence: dispatch.sequence });
|
|
265
|
+
}
|
|
266
|
+
yield* application.awaitShellSequence(dispatch.sequence);
|
|
267
|
+
yield* waitForAskThread(application, output, {
|
|
268
|
+
threadId,
|
|
269
|
+
format: resolvedFormat,
|
|
270
|
+
messageId: askMessageId,
|
|
271
|
+
state,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const finalSnapshot = yield* application.getThreadMessages({ threadId });
|
|
275
|
+
const observation = inspectAskTurn(finalSnapshot.thread, askMessageId, state.askTurnId);
|
|
276
|
+
if (observation.status === "failed") {
|
|
277
|
+
return yield* Effect.fail(
|
|
278
|
+
new ThreadSessionError({
|
|
279
|
+
threadId,
|
|
280
|
+
message: observation.message,
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const answer = selectAskAnswer(finalSnapshot.thread, askMessageId, observation.turnId);
|
|
285
|
+
if (answer === undefined) {
|
|
286
|
+
return yield* Effect.fail(
|
|
287
|
+
new AskNoAnswerError({
|
|
288
|
+
message: `thread completed without a new final answer: ${threadId}`,
|
|
289
|
+
threadId,
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
answer: answer.text,
|
|
295
|
+
threadId,
|
|
296
|
+
turnId: answer.turnId,
|
|
297
|
+
created,
|
|
298
|
+
dispatch,
|
|
299
|
+
} satisfies AskResponse;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const responseWithCleanup = response.pipe(
|
|
303
|
+
Effect.onInterrupt(() => cleanupInterruptedAsk(application, output, state)),
|
|
304
|
+
);
|
|
305
|
+
const timedResponse =
|
|
306
|
+
timeoutDuration === undefined
|
|
307
|
+
? responseWithCleanup
|
|
308
|
+
: responseWithCleanup.pipe(
|
|
309
|
+
Effect.timeoutOrElse({
|
|
310
|
+
duration: timeoutDuration,
|
|
311
|
+
orElse: () =>
|
|
312
|
+
Effect.fail(
|
|
313
|
+
new AskTimeoutError({
|
|
314
|
+
message: `ask timed out after ${timeoutValue ?? Duration.format(timeoutDuration)}`,
|
|
315
|
+
timeout: timeoutValue ?? Duration.format(timeoutDuration),
|
|
316
|
+
}),
|
|
317
|
+
),
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return yield* timedResponse.pipe(
|
|
322
|
+
Effect.matchEffect({
|
|
323
|
+
onFailure: (error) =>
|
|
324
|
+
finalizeArchive(application, output, state, false).pipe(
|
|
325
|
+
Effect.flatMap(() => Effect.fail(error)),
|
|
326
|
+
),
|
|
327
|
+
onSuccess: (result) =>
|
|
328
|
+
Effect.gen(function* () {
|
|
329
|
+
const archiveResult = yield* finalizeArchive(application, output, state, true);
|
|
330
|
+
const formatted = {
|
|
331
|
+
answer: result.answer,
|
|
332
|
+
threadId: result.threadId,
|
|
333
|
+
turnId: result.turnId,
|
|
334
|
+
created: result.created,
|
|
335
|
+
dispatch: result.dispatch,
|
|
336
|
+
archive: archiveResult,
|
|
337
|
+
};
|
|
338
|
+
if (resolvedFormat === "json") {
|
|
339
|
+
return yield* output.printJson(formatted);
|
|
340
|
+
}
|
|
341
|
+
if (resolvedFormat === "ndjson") {
|
|
342
|
+
return yield* output.printNdjson({ type: "result", ...formatted });
|
|
343
|
+
}
|
|
344
|
+
return yield* output.writeStdout(ensureTrailingNewline(result.answer));
|
|
345
|
+
}),
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
}),
|
|
349
|
+
).pipe(Command.withDescription("ask a project or existing thread and wait for one answer"));
|
|
350
|
+
|
|
351
|
+
function parseAskTimeout(value: string) {
|
|
352
|
+
const trimmed = value.trim();
|
|
353
|
+
const shorthand = /^([0-9]+(?:\.[0-9]+)?)(ms|s|m|h|d|w)$/.exec(trimmed);
|
|
354
|
+
let input = trimmed;
|
|
355
|
+
if (shorthand !== null) {
|
|
356
|
+
const amount = shorthand[1];
|
|
357
|
+
const unit = shorthand[2];
|
|
358
|
+
const units: Readonly<Record<string, string>> = {
|
|
359
|
+
ms: "millis",
|
|
360
|
+
s: "seconds",
|
|
361
|
+
m: "minutes",
|
|
362
|
+
h: "hours",
|
|
363
|
+
d: "days",
|
|
364
|
+
w: "weeks",
|
|
365
|
+
};
|
|
366
|
+
const expandedUnit = unit === undefined ? undefined : units[unit];
|
|
367
|
+
if (amount !== undefined && expandedUnit !== undefined) {
|
|
368
|
+
input = `${amount} ${expandedUnit}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const parsed = Schema.decodeUnknownOption(Schema.DurationFromString)(input);
|
|
372
|
+
if (Option.isNone(parsed)) {
|
|
373
|
+
return Effect.fail(
|
|
374
|
+
new InvalidAskTimeoutError({
|
|
375
|
+
message: `invalid timeout: ${value}`,
|
|
376
|
+
value,
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const millis = Duration.toMillis(parsed.value);
|
|
381
|
+
if (!Number.isFinite(millis) || millis <= 0) {
|
|
382
|
+
return Effect.fail(
|
|
383
|
+
new InvalidAskTimeoutError({
|
|
384
|
+
message: `timeout must be a positive finite duration: ${value}`,
|
|
385
|
+
value,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
return Effect.succeed(parsed.value);
|
|
390
|
+
}
|
package/src/cli/error.ts
CHANGED
|
@@ -39,6 +39,60 @@ export class InvalidFlagCombinationError extends Schema.TaggedErrorClass<Invalid
|
|
|
39
39
|
},
|
|
40
40
|
) {}
|
|
41
41
|
|
|
42
|
+
export class InvalidAskTimeoutError extends Schema.TaggedErrorClass<InvalidAskTimeoutError>()(
|
|
43
|
+
"InvalidAskTimeoutError",
|
|
44
|
+
{
|
|
45
|
+
message: Schema.String,
|
|
46
|
+
value: Schema.String,
|
|
47
|
+
},
|
|
48
|
+
) {}
|
|
49
|
+
|
|
50
|
+
export class AskThreadArchivedError extends Schema.TaggedErrorClass<AskThreadArchivedError>()(
|
|
51
|
+
"AskThreadArchivedError",
|
|
52
|
+
{
|
|
53
|
+
message: Schema.String,
|
|
54
|
+
threadId: Schema.String,
|
|
55
|
+
},
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
export class AskThreadBusyError extends Schema.TaggedErrorClass<AskThreadBusyError>()(
|
|
59
|
+
"AskThreadBusyError",
|
|
60
|
+
{
|
|
61
|
+
message: Schema.String,
|
|
62
|
+
threadId: Schema.String,
|
|
63
|
+
},
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
export class AskThreadPendingRequestError extends Schema.TaggedErrorClass<AskThreadPendingRequestError>()(
|
|
67
|
+
"AskThreadPendingRequestError",
|
|
68
|
+
{
|
|
69
|
+
message: Schema.String,
|
|
70
|
+
threadId: Schema.String,
|
|
71
|
+
},
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
export class AskProjectMismatchError extends Schema.TaggedErrorClass<AskProjectMismatchError>()(
|
|
75
|
+
"AskProjectMismatchError",
|
|
76
|
+
{
|
|
77
|
+
message: Schema.String,
|
|
78
|
+
threadId: Schema.String,
|
|
79
|
+
projectId: Schema.String,
|
|
80
|
+
},
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
export class AskNoAnswerError extends Schema.TaggedErrorClass<AskNoAnswerError>()(
|
|
84
|
+
"AskNoAnswerError",
|
|
85
|
+
{
|
|
86
|
+
message: Schema.String,
|
|
87
|
+
threadId: Schema.String,
|
|
88
|
+
},
|
|
89
|
+
) {}
|
|
90
|
+
|
|
91
|
+
export class AskTimeoutError extends Schema.TaggedErrorClass<AskTimeoutError>()("AskTimeoutError", {
|
|
92
|
+
message: Schema.String,
|
|
93
|
+
timeout: Schema.String,
|
|
94
|
+
}) {}
|
|
95
|
+
|
|
42
96
|
export class MissingRequestError extends Schema.TaggedErrorClass<MissingRequestError>()(
|
|
43
97
|
"MissingRequestError",
|
|
44
98
|
{
|
|
@@ -59,3 +113,19 @@ export class ConflictingUpdateFlagsError extends Schema.TaggedErrorClass<Conflic
|
|
|
59
113
|
message: Schema.String,
|
|
60
114
|
},
|
|
61
115
|
) {}
|
|
116
|
+
|
|
117
|
+
export class InvalidSnoozeUntilError extends Schema.TaggedErrorClass<InvalidSnoozeUntilError>()(
|
|
118
|
+
"InvalidSnoozeUntilError",
|
|
119
|
+
{
|
|
120
|
+
message: Schema.String,
|
|
121
|
+
value: Schema.String,
|
|
122
|
+
},
|
|
123
|
+
) {}
|
|
124
|
+
|
|
125
|
+
export class UnavailableSnoozePresetError extends Schema.TaggedErrorClass<UnavailableSnoozePresetError>()(
|
|
126
|
+
"UnavailableSnoozePresetError",
|
|
127
|
+
{
|
|
128
|
+
message: Schema.String,
|
|
129
|
+
preset: Schema.String,
|
|
130
|
+
},
|
|
131
|
+
) {}
|
package/src/cli/format/thread.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { ThreadShow } from "../../application/threads.ts";
|
|
1
|
+
import type { ThreadSearchResult, ThreadShow } from "../../application/threads.ts";
|
|
2
2
|
import type { WaitEvent } from "../../application/service.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
OrchestrationThread,
|
|
5
|
+
OrchestrationThreadDetailSnapshot,
|
|
6
|
+
OrchestrationThreadShell,
|
|
7
|
+
} from "@t3tools/contracts";
|
|
4
8
|
import { latestAssistantMessage, threadStatus } from "../../domain/thread-lifecycle.ts";
|
|
5
9
|
import { formatChatTranscript, formatRecord, formatTable } from "./human.ts";
|
|
6
10
|
|
|
@@ -98,6 +102,28 @@ export function formatThreadsHuman(threads: ReadonlyArray<OrchestrationThreadShe
|
|
|
98
102
|
)}\n`;
|
|
99
103
|
}
|
|
100
104
|
|
|
105
|
+
export function formatThreadSearchHuman(matches: ReadonlyArray<ThreadSearchResult>) {
|
|
106
|
+
if (matches.length === 0) {
|
|
107
|
+
return "no matches\n";
|
|
108
|
+
}
|
|
109
|
+
return `${matches
|
|
110
|
+
.map((match) =>
|
|
111
|
+
formatRecord([
|
|
112
|
+
{ field: "thread", value: match.threadTitle ?? "-" },
|
|
113
|
+
{ field: "thread id", value: match.threadId },
|
|
114
|
+
{ field: "project", value: match.projectTitle ?? "-" },
|
|
115
|
+
{ field: "project id", value: match.projectId },
|
|
116
|
+
{ field: "workspace", value: match.workspaceRoot ?? "-" },
|
|
117
|
+
{ field: "branch", value: match.branch ?? "-" },
|
|
118
|
+
{ field: "worktree", value: match.worktreePath ?? "-" },
|
|
119
|
+
{ field: "source", value: match.source },
|
|
120
|
+
{ field: "created", value: match.messageCreatedAt ?? "-" },
|
|
121
|
+
{ field: "snippet", value: match.snippet },
|
|
122
|
+
]),
|
|
123
|
+
)
|
|
124
|
+
.join("\n\n")}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
101
127
|
export function formatThreadDeletedHuman(input: {
|
|
102
128
|
readonly threadId: string;
|
|
103
129
|
readonly dispatch: { readonly sequence: number };
|
|
@@ -117,9 +143,15 @@ export function formatThreadStartedHuman(input: {
|
|
|
117
143
|
])}`;
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
export function formatThreadMessagesHuman(
|
|
121
|
-
|
|
122
|
-
|
|
146
|
+
export function formatThreadMessagesHuman(
|
|
147
|
+
snapshot: OrchestrationThreadDetailSnapshot,
|
|
148
|
+
limit: number,
|
|
149
|
+
) {
|
|
150
|
+
const messages = limit === 0 ? snapshot.thread.messages : snapshot.thread.messages.slice(-limit);
|
|
151
|
+
const transcript = formatChatTranscript(messages);
|
|
152
|
+
return snapshot.page?.hasMore === true && snapshot.page.beforeCursor !== null
|
|
153
|
+
? `${transcript}\nearlier turns available\nbefore cursor: ${snapshot.page.beforeCursor}\n`
|
|
154
|
+
: transcript;
|
|
123
155
|
}
|
|
124
156
|
|
|
125
157
|
export function formatWaitDoneHuman(thread: OrchestrationThread) {
|
|
@@ -129,8 +161,18 @@ export function formatWaitDoneHuman(thread: OrchestrationThread) {
|
|
|
129
161
|
}`;
|
|
130
162
|
}
|
|
131
163
|
|
|
132
|
-
export function formatThreadMessagesJson(
|
|
133
|
-
|
|
164
|
+
export function formatThreadMessagesJson(
|
|
165
|
+
snapshot: OrchestrationThreadDetailSnapshot,
|
|
166
|
+
full: boolean,
|
|
167
|
+
) {
|
|
168
|
+
return full
|
|
169
|
+
? { ...snapshot, page: snapshot.page ?? null }
|
|
170
|
+
: {
|
|
171
|
+
snapshotSequence: snapshot.snapshotSequence,
|
|
172
|
+
thread: stripThreadMessages(snapshot.thread),
|
|
173
|
+
messages: snapshot.thread.messages,
|
|
174
|
+
page: snapshot.page ?? null,
|
|
175
|
+
};
|
|
134
176
|
}
|
|
135
177
|
|
|
136
178
|
export function formatWaitEventNdjson(event: WaitEvent) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
|
+
|
|
5
|
+
import { T3Application } from "../application/service.ts";
|
|
6
|
+
import { loadT3CliEnv } from "../config/env/env.ts";
|
|
7
|
+
import { InvalidLimitError } from "./error.ts";
|
|
8
|
+
import { extraArgsConfig } from "./extra-args.ts";
|
|
9
|
+
import { formatFlag } from "./flags.ts";
|
|
10
|
+
import { formatThreadSearchHuman } from "./format/thread.ts";
|
|
11
|
+
import { resolveOutputFormat } from "./format/output.ts";
|
|
12
|
+
import { T3Output } from "./output/service.ts";
|
|
13
|
+
import { CliRuntime } from "./runtime/service.ts";
|
|
14
|
+
|
|
15
|
+
export const searchThreadsCommand = Command.make(
|
|
16
|
+
"search",
|
|
17
|
+
{
|
|
18
|
+
query: Argument.string("query"),
|
|
19
|
+
limit: Flag.integer("limit").pipe(
|
|
20
|
+
Flag.withDescription("Maximum matches (1-50, default: 50)"),
|
|
21
|
+
Flag.optional,
|
|
22
|
+
),
|
|
23
|
+
format: formatFlag,
|
|
24
|
+
...extraArgsConfig,
|
|
25
|
+
},
|
|
26
|
+
({ query, limit, format }) =>
|
|
27
|
+
Effect.gen(function* () {
|
|
28
|
+
const value = Option.getOrUndefined(limit);
|
|
29
|
+
if (value !== undefined && (value < 1 || value > 50)) {
|
|
30
|
+
return yield* Effect.fail(
|
|
31
|
+
new InvalidLimitError({ message: `invalid limit: ${value}`, value: String(value) }),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const application = yield* T3Application;
|
|
35
|
+
const cliRuntime = yield* CliRuntime;
|
|
36
|
+
const t3CliEnv = yield* loadT3CliEnv;
|
|
37
|
+
const output = yield* T3Output;
|
|
38
|
+
const resolvedFormat = resolveOutputFormat(format, cliRuntime, t3CliEnv, "json");
|
|
39
|
+
const matches = yield* application.searchThreads({
|
|
40
|
+
query,
|
|
41
|
+
...(value !== undefined ? { limit: value } : {}),
|
|
42
|
+
});
|
|
43
|
+
if (resolvedFormat === "json") {
|
|
44
|
+
return yield* output.printJson(matches);
|
|
45
|
+
}
|
|
46
|
+
return yield* output.writeStdout(formatThreadSearchHuman(matches));
|
|
47
|
+
}),
|
|
48
|
+
).pipe(Command.withDescription("search threads by conversation content"));
|
package/src/cli/thread.ts
CHANGED
|
@@ -5,8 +5,14 @@ import { archiveThreadCommand } from "./threads/archive.ts";
|
|
|
5
5
|
import { callbackThreadCommand } from "./threads/callback.ts";
|
|
6
6
|
import { deleteThreadCommand } from "./threads/delete.ts";
|
|
7
7
|
import { interruptThreadCommand } from "./threads/interrupt.ts";
|
|
8
|
+
import { pinThreadCommand } from "./threads/pin.ts";
|
|
8
9
|
import { respondThreadCommand } from "./threads/respond.ts";
|
|
10
|
+
import { settleThreadCommand } from "./threads/settle.ts";
|
|
11
|
+
import { snoozeThreadCommand } from "./threads/snooze.ts";
|
|
9
12
|
import { unarchiveThreadCommand } from "./threads/unarchive.ts";
|
|
13
|
+
import { unpinThreadCommand } from "./threads/unpin.ts";
|
|
14
|
+
import { unsnoozeThreadCommand } from "./threads/unsnooze.ts";
|
|
15
|
+
import { unsettleThreadCommand } from "./threads/unsettle.ts";
|
|
10
16
|
import { updateThreadCommand } from "./threads/update.ts";
|
|
11
17
|
|
|
12
18
|
export function createThreadCommand() {
|
|
@@ -17,6 +23,12 @@ export function createThreadCommand() {
|
|
|
17
23
|
respondThreadCommand,
|
|
18
24
|
archiveThreadCommand,
|
|
19
25
|
interruptThreadCommand,
|
|
26
|
+
settleThreadCommand,
|
|
27
|
+
unsettleThreadCommand,
|
|
28
|
+
snoozeThreadCommand,
|
|
29
|
+
unsnoozeThreadCommand,
|
|
30
|
+
pinThreadCommand,
|
|
31
|
+
unpinThreadCommand,
|
|
20
32
|
unarchiveThreadCommand,
|
|
21
33
|
updateThreadCommand,
|
|
22
34
|
deleteThreadCommand,
|
|
@@ -4,7 +4,7 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
4
4
|
|
|
5
5
|
import { extraArgsConfig } from "../extra-args.ts";
|
|
6
6
|
import { formatFlag, threadFlag } from "../flags.ts";
|
|
7
|
-
import { InvalidLimitError } from "../error.ts";
|
|
7
|
+
import { InvalidFlagCombinationError, InvalidLimitError } from "../error.ts";
|
|
8
8
|
import { MissingThreadError } from "../error.ts";
|
|
9
9
|
import { resolveThreadId } from "../scope/index.ts";
|
|
10
10
|
import { formatThreadMessagesHuman, formatThreadMessagesJson } from "../format/thread.ts";
|
|
@@ -19,17 +19,37 @@ export const getThreadTranscriptCommand = Command.make(
|
|
|
19
19
|
{
|
|
20
20
|
thread: threadFlag,
|
|
21
21
|
limit: Flag.integer("limit").pipe(Flag.withDefault(20)),
|
|
22
|
+
turnLimit: Flag.integer("turn-limit").pipe(Flag.optional),
|
|
23
|
+
beforeCursor: Flag.string("before-cursor").pipe(Flag.optional),
|
|
24
|
+
all: Flag.boolean("all"),
|
|
22
25
|
full: Flag.boolean("full"),
|
|
23
26
|
format: formatFlag,
|
|
24
27
|
...extraArgsConfig,
|
|
25
28
|
},
|
|
26
|
-
({ thread, limit, full, format }) =>
|
|
29
|
+
({ thread, limit, turnLimit, beforeCursor, all, full, format }) =>
|
|
27
30
|
Effect.gen(function* () {
|
|
28
31
|
if (limit < 0) {
|
|
29
32
|
return yield* Effect.fail(
|
|
30
33
|
new InvalidLimitError({ message: `invalid limit: ${limit}`, value: String(limit) }),
|
|
31
34
|
);
|
|
32
35
|
}
|
|
36
|
+
const turnLimitValue = Option.getOrUndefined(turnLimit);
|
|
37
|
+
const beforeCursorValue = Option.getOrUndefined(beforeCursor);
|
|
38
|
+
if (turnLimitValue !== undefined && turnLimitValue <= 0) {
|
|
39
|
+
return yield* Effect.fail(
|
|
40
|
+
new InvalidLimitError({
|
|
41
|
+
message: `invalid turn limit: ${turnLimitValue}`,
|
|
42
|
+
value: String(turnLimitValue),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (all && (turnLimitValue !== undefined || beforeCursorValue !== undefined)) {
|
|
47
|
+
return yield* Effect.fail(
|
|
48
|
+
new InvalidFlagCombinationError({
|
|
49
|
+
message: "--all cannot be combined with --turn-limit or --before-cursor",
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
33
53
|
const application = yield* T3Application;
|
|
34
54
|
const cliRuntime = yield* CliRuntime;
|
|
35
55
|
const t3CliEnv = yield* loadT3CliEnv;
|
|
@@ -46,7 +66,17 @@ export const getThreadTranscriptCommand = Command.make(
|
|
|
46
66
|
);
|
|
47
67
|
}
|
|
48
68
|
const resolvedFormat = resolveOutputFormat(format, cliRuntime, t3CliEnv, "json");
|
|
49
|
-
const detail = yield* application.getThreadMessages(
|
|
69
|
+
const detail = yield* application.getThreadMessages({
|
|
70
|
+
threadId,
|
|
71
|
+
...(!all
|
|
72
|
+
? {
|
|
73
|
+
window: {
|
|
74
|
+
turnLimit: turnLimitValue ?? (beforeCursorValue === undefined ? 10 : 20),
|
|
75
|
+
...(beforeCursorValue !== undefined ? { beforeCursor: beforeCursorValue } : {}),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
});
|
|
50
80
|
if (resolvedFormat === "json") {
|
|
51
81
|
return yield* output.printJson(formatThreadMessagesJson(detail, full));
|
|
52
82
|
}
|