@gajae-code/coding-agent 0.8.2 → 0.9.1
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/CHANGELOG.md +53 -0
- package/dist/types/append-only-mode.d.ts +31 -0
- package/dist/types/capability/rule.d.ts +8 -0
- package/dist/types/cli/fixture-report.d.ts +20 -0
- package/dist/types/cli/fixture-report.test.d.ts +1 -0
- package/dist/types/cli/nofile-limit.d.ts +11 -0
- package/dist/types/cli/update-cli.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +119 -1
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/export/ttsr.d.ts +14 -3
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
- package/dist/types/goals/runtime.d.ts +1 -0
- package/dist/types/hooks/native-skill-hook.d.ts +12 -0
- package/dist/types/modes/components/assistant-message.d.ts +5 -1
- package/dist/types/modes/components/status-line.d.ts +9 -0
- package/dist/types/modes/components/thinking-selector.d.ts +2 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +5 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/notifications/config-commands.d.ts +9 -0
- package/dist/types/notifications/config.d.ts +22 -0
- package/dist/types/notifications/index.d.ts +2 -0
- package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
- package/dist/types/notifications/reply-sent-store.d.ts +53 -0
- package/dist/types/notifications/rich-draft.d.ts +68 -0
- package/dist/types/notifications/rich-render.d.ts +90 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/telegram-daemon.d.ts +30 -0
- package/dist/types/notifications/telegram-reference.d.ts +7 -0
- package/dist/types/notifications/threaded-render.d.ts +14 -0
- package/dist/types/sdk.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +43 -1
- package/dist/types/session/file-mention-pruning.d.ts +8 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/skill-state/active-state.d.ts +18 -1
- package/dist/types/system-prompt.d.ts +18 -2
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/fork-context-budget.d.ts +7 -0
- package/dist/types/task/token-log.d.ts +30 -0
- package/dist/types/task/token-log.test.d.ts +1 -0
- package/dist/types/tools/bisect.d.ts +81 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/utils/file-mentions.d.ts +14 -0
- package/dist/types/utils/git.d.ts +32 -0
- package/dist/types/utils/pasted-image-path.d.ts +30 -0
- package/dist/types/web/scrapers/youtube.d.ts +8 -0
- package/package.json +8 -11
- package/src/append-only-mode.ts +58 -0
- package/src/autoresearch/prompt.md +1 -1
- package/src/autoresearch/tools/log-experiment.ts +1 -1
- package/src/capability/rule.ts +9 -0
- package/src/cli/config-cli.ts +27 -3
- package/src/cli/fixture-report.test.ts +230 -0
- package/src/cli/fixture-report.ts +171 -0
- package/src/cli/nofile-limit.ts +48 -0
- package/src/cli/notify-cli.ts +11 -9
- package/src/cli/update-cli.ts +85 -9
- package/src/cli.ts +26 -0
- package/src/commands/contribution-prep.ts +1 -1
- package/src/config/keybindings.ts +1 -1
- package/src/config/settings-schema.ts +76 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
- package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
- package/src/defaults/gjc/skills/team/SKILL.md +16 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
- package/src/defaults/gjc-defaults.ts +9 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/index.ts +1 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +60 -2
- package/src/export/ttsr.ts +96 -18
- package/src/extensibility/extensions/runner.ts +181 -219
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
- package/src/gjc-runtime/deep-interview-state.ts +31 -2
- package/src/gjc-runtime/launch-tmux.ts +6 -0
- package/src/gjc-runtime/session-state-sidecar.ts +208 -13
- package/src/gjc-runtime/state-writer.ts +15 -0
- package/src/gjc-runtime/team-runtime.ts +175 -19
- package/src/gjc-runtime/tmux-common.ts +1 -0
- package/src/gjc-runtime/ultragoal-guard.ts +55 -66
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
- package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
- package/src/gjc-runtime/workflow-manifest.ts +74 -1
- package/src/goals/runtime.ts +10 -2
- package/src/hindsight/backend.ts +1 -3
- package/src/hooks/native-skill-hook.ts +95 -4
- package/src/internal-urls/docs-index.generated.ts +18 -9
- package/src/lsp/client.ts +1 -0
- package/src/main.ts +43 -2
- package/src/memories/index.ts +12 -1
- package/src/modes/components/assistant-message.ts +241 -82
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +7 -2
- package/src/modes/components/status-line.ts +167 -54
- package/src/modes/components/thinking-selector.ts +10 -8
- package/src/modes/components/tool-execution.ts +44 -19
- package/src/modes/controllers/command-controller.ts +39 -2
- package/src/modes/controllers/event-controller.ts +77 -49
- package/src/modes/controllers/extension-ui-controller.ts +8 -1
- package/src/modes/controllers/input-controller.ts +13 -18
- package/src/modes/controllers/selector-controller.ts +41 -0
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/rpc/rpc-mode.ts +32 -12
- package/src/modes/rpc/rpc-socket-security.ts +13 -1
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/modes/shared/agent-wire/command-validation.ts +4 -0
- package/src/modes/shared/agent-wire/event-contract.ts +22 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/utils/injected-user-submission.ts +94 -0
- package/src/modes/utils/ui-helpers.ts +17 -4
- package/src/notifications/config-commands.ts +20 -0
- package/src/notifications/config.ts +43 -4
- package/src/notifications/index.ts +105 -9
- package/src/notifications/lifecycle-commands.ts +37 -16
- package/src/notifications/lifecycle-control-runtime.ts +8 -3
- package/src/notifications/reply-sent-store.ts +134 -0
- package/src/notifications/rich-draft.ts +107 -0
- package/src/notifications/rich-render.ts +142 -0
- package/src/notifications/telegram-daemon-cli.ts +54 -7
- package/src/notifications/telegram-daemon-control.ts +2 -2
- package/src/notifications/telegram-daemon.ts +390 -56
- package/src/notifications/telegram-reference.ts +17 -0
- package/src/notifications/threaded-render.ts +45 -2
- package/src/prompts/agents/architect.md +19 -10
- package/src/prompts/agents/critic.md +16 -18
- package/src/prompts/agents/executor.md +14 -11
- package/src/prompts/agents/planner.md +18 -13
- package/src/prompts/ci-green-request.md +3 -8
- package/src/prompts/goals/goal-continuation.md +0 -5
- package/src/prompts/goals/goal-mode-active.md +0 -5
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/system/plan-mode-subagent.md +3 -3
- package/src/prompts/system/project-prompt.md +0 -13
- package/src/prompts/system/subagent-system-prompt.md +3 -2
- package/src/prompts/system/system-prompt.md +26 -19
- package/src/prompts/system/volatile-project-context.md +10 -0
- package/src/prompts/system/web-search.md +4 -7
- package/src/prompts/tools/apply-patch.md +6 -0
- package/src/prompts/tools/ast-edit.md +1 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/bisect.md +22 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/cron.md +0 -2
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/github.md +6 -6
- package/src/prompts/tools/goal.md +1 -3
- package/src/prompts/tools/image-gen.md +12 -5
- package/src/prompts/tools/irc.md +5 -4
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/lsp.md +1 -1
- package/src/prompts/tools/monitor.md +2 -2
- package/src/prompts/tools/patch.md +2 -2
- package/src/prompts/tools/replace.md +2 -21
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search-tool-bm25.md +6 -11
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/skill.md +3 -2
- package/src/prompts/tools/ssh.md +2 -1
- package/src/prompts/tools/subagent.md +2 -2
- package/src/prompts/tools/task.md +4 -5
- package/src/prompts/tools/todo-write.md +1 -1
- package/src/prompts/tools/vim.md +1 -0
- package/src/prompts/tools/web-search.md +2 -0
- package/src/prompts/tools/write.md +8 -2
- package/src/sdk.ts +68 -30
- package/src/session/agent-session.ts +734 -107
- package/src/session/file-mention-pruning.ts +64 -0
- package/src/session/messages.ts +4 -0
- package/src/session/session-manager.ts +59 -2
- package/src/skill-state/active-state.ts +141 -5
- package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
- package/src/slash-commands/builtin-registry.ts +143 -1
- package/src/system-prompt.ts +57 -16
- package/src/task/executor.ts +125 -20
- package/src/task/fork-context-advisory.ts +2 -7
- package/src/task/fork-context-budget.ts +9 -0
- package/src/task/index.ts +4 -3
- package/src/task/output-manager.ts +4 -2
- package/src/task/token-log.test.ts +188 -0
- package/src/task/token-log.ts +179 -0
- package/src/tools/bisect.ts +367 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/output-meta.ts +116 -4
- package/src/tools/search.ts +8 -2
- package/src/tools/skill.ts +15 -0
- package/src/utils/file-mentions.ts +39 -7
- package/src/utils/git.ts +73 -0
- package/src/utils/pasted-image-path.ts +170 -0
- package/src/web/scrapers/youtube.ts +24 -12
|
@@ -12,6 +12,9 @@ import { randomUUID } from "node:crypto";
|
|
|
12
12
|
import type { AgentSessionEvent } from "../../../session/agent-session";
|
|
13
13
|
import {
|
|
14
14
|
AGENT_WIRE_PROTOCOL_VERSION,
|
|
15
|
+
type AgentWireCompactAssistantMessageEvent,
|
|
16
|
+
type AgentWireCompactEventFrame,
|
|
17
|
+
type AgentWireCompactMessageUpdatePayload,
|
|
15
18
|
type AgentWireEventFrame,
|
|
16
19
|
type AgentWireEventPayload,
|
|
17
20
|
type AgentWireEventType,
|
|
@@ -19,6 +22,26 @@ import {
|
|
|
19
22
|
type AgentWireFrameType,
|
|
20
23
|
} from "./event-contract";
|
|
21
24
|
|
|
25
|
+
export const AGENT_WIRE_COMPACT_MESSAGE_UPDATE_CHECKPOINT_INTERVAL = 32;
|
|
26
|
+
|
|
27
|
+
function recordObject(value: unknown): Record<string, unknown> | undefined {
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function messageIdOf(message: unknown): string | null {
|
|
32
|
+
const id = recordObject(message)?.id;
|
|
33
|
+
return typeof id === "string" ? id : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function contentIndexOf(assistantMessageEvent: unknown): number | null {
|
|
37
|
+
const contentIndex = recordObject(assistantMessageEvent)?.contentIndex;
|
|
38
|
+
return typeof contentIndex === "number" && Number.isInteger(contentIndex) ? contentIndex : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function snapshotMessage<T>(message: T): T {
|
|
42
|
+
return structuredClone(message);
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
function assertNever(value: never): never {
|
|
23
46
|
throw new Error(`Unhandled AgentSessionEvent variant: ${JSON.stringify(value)}`);
|
|
24
47
|
}
|
|
@@ -123,6 +146,65 @@ export function toAgentWireEventFrame(
|
|
|
123
146
|
export function toAgentWireEventPayload(event: AgentSessionEvent): AgentWireEventPayload {
|
|
124
147
|
return { event_type: agentSessionEventType(event), event };
|
|
125
148
|
}
|
|
149
|
+
export function sanitizeAssistantMessageEventForCompact(
|
|
150
|
+
assistantMessageEvent: Extract<AgentSessionEvent, { type: "message_update" }>["assistantMessageEvent"],
|
|
151
|
+
): AgentWireCompactAssistantMessageEvent {
|
|
152
|
+
const {
|
|
153
|
+
partial: _partial,
|
|
154
|
+
message: _message,
|
|
155
|
+
error: _error,
|
|
156
|
+
...deltaOnly
|
|
157
|
+
} = assistantMessageEvent as Record<string, unknown>;
|
|
158
|
+
return deltaOnly as AgentWireCompactAssistantMessageEvent;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface AgentWireCompactMessageUpdateOptions {
|
|
162
|
+
checkpointInterval?: number;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stateful compact event serializer for opt-in RPC clients.
|
|
167
|
+
*
|
|
168
|
+
* Only `message_update` changes shape: it carries the immutable provider delta
|
|
169
|
+
* plus message/content identifiers, with periodic full-message checkpoints that
|
|
170
|
+
* are cloned synchronously before the caller returns to the event loop. All other
|
|
171
|
+
* event variants remain canonical full event frames so legacy bridge/RPC behavior
|
|
172
|
+
* and terminal `message_end` reconstruction are unchanged.
|
|
173
|
+
*/
|
|
174
|
+
export class AgentWireCompactEventEncoder {
|
|
175
|
+
readonly #sequencer: AgentWireFrameSequencer;
|
|
176
|
+
readonly #checkpointInterval: number;
|
|
177
|
+
#messageUpdateCount = 0;
|
|
178
|
+
|
|
179
|
+
constructor(sequencer: AgentWireFrameSequencer, options: AgentWireCompactMessageUpdateOptions = {}) {
|
|
180
|
+
this.#sequencer = sequencer;
|
|
181
|
+
this.#checkpointInterval = options.checkpointInterval ?? AGENT_WIRE_COMPACT_MESSAGE_UPDATE_CHECKPOINT_INTERVAL;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
frame(event: AgentSessionEvent): AgentWireCompactEventFrame {
|
|
185
|
+
if (event.type !== "message_update") return toAgentWireEventFrame(event, this.#sequencer);
|
|
186
|
+
this.#messageUpdateCount += 1;
|
|
187
|
+
const payload: AgentWireCompactMessageUpdatePayload = {
|
|
188
|
+
event_type: "message_update",
|
|
189
|
+
compact: true,
|
|
190
|
+
message_id: messageIdOf(event.message),
|
|
191
|
+
content_index: contentIndexOf(event.assistantMessageEvent),
|
|
192
|
+
assistantMessageEvent: sanitizeAssistantMessageEventForCompact(event.assistantMessageEvent),
|
|
193
|
+
};
|
|
194
|
+
if (this.#checkpointInterval > 0 && this.#messageUpdateCount % this.#checkpointInterval === 0) {
|
|
195
|
+
payload.checkpoint_message = snapshotMessage(event.message);
|
|
196
|
+
payload.checkpoint_reason = "periodic";
|
|
197
|
+
}
|
|
198
|
+
return this.#sequencer.next("event", payload);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function toAgentWireCompactEventFrame(
|
|
203
|
+
event: AgentSessionEvent,
|
|
204
|
+
encoder: AgentWireCompactEventEncoder,
|
|
205
|
+
): AgentWireCompactEventFrame {
|
|
206
|
+
return encoder.frame(event);
|
|
207
|
+
}
|
|
126
208
|
|
|
127
209
|
/** Back-compat alias for {@link toAgentWireEventFrame}. */
|
|
128
210
|
export const toBridgeEventFrame = toAgentWireEventFrame;
|
|
@@ -47,6 +47,7 @@ const RPC_COMMAND_SCOPE_REGISTRY: Record<RpcCommandType, BridgeCommandScope> = {
|
|
|
47
47
|
set_host_tools: "host_tools",
|
|
48
48
|
set_host_uri_schemes: "host_uri",
|
|
49
49
|
get_pending_workflow_gates: "message:read",
|
|
50
|
+
set_capabilities: "control",
|
|
50
51
|
set_model: "model",
|
|
51
52
|
cycle_model: "model",
|
|
52
53
|
get_available_models: "model",
|
package/src/modes/types.ts
CHANGED
|
@@ -116,6 +116,7 @@ export interface InteractiveModeContext {
|
|
|
116
116
|
onInputCallback?: (input: SubmittedUserInput) => void;
|
|
117
117
|
optimisticUserMessageSignature: string | undefined;
|
|
118
118
|
locallySubmittedUserSignatures: Set<string>;
|
|
119
|
+
optimisticInjectedSignatures: Map<string, number>;
|
|
119
120
|
lastSigintTime: number;
|
|
120
121
|
lastEscapeTime: number;
|
|
121
122
|
lastComposerClearEscapeTime: number;
|
|
@@ -216,6 +217,7 @@ export interface InteractiveModeContext {
|
|
|
216
217
|
handleDumpCommand(): void;
|
|
217
218
|
handleDebugTranscriptCommand(): Promise<void>;
|
|
218
219
|
handleClearCommand(): Promise<void>;
|
|
220
|
+
handleContextClearCommand(): Promise<void>;
|
|
219
221
|
handleDropCommand(): Promise<void>;
|
|
220
222
|
handleForkCommand(): Promise<void>;
|
|
221
223
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
|
|
@@ -243,6 +245,7 @@ export interface InteractiveModeContext {
|
|
|
243
245
|
showExtensionsDashboard(): void;
|
|
244
246
|
showAgentsDashboard(): void;
|
|
245
247
|
showModelSelector(options?: { temporaryOnly?: boolean }): void;
|
|
248
|
+
showEffortSelector(): void;
|
|
246
249
|
showProviderOnboarding(): void;
|
|
247
250
|
showPluginSelector(mode?: "install" | "uninstall"): void;
|
|
248
251
|
showUserMessageSelector(): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
2
|
+
import type { InteractiveModeContext } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalize the content passed to an extension `sendUserMessage` call into a
|
|
6
|
+
* plain text string plus its image attachments. Mirrors the normalization in
|
|
7
|
+
* `AgentSession.sendUserMessage` (text parts joined with "\n") so the resulting
|
|
8
|
+
* text matches the eventual user `message_start` payload.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeInjectedUserContent(content: string | (TextContent | ImageContent)[]): {
|
|
11
|
+
text: string;
|
|
12
|
+
images: ImageContent[];
|
|
13
|
+
imageCount: number;
|
|
14
|
+
} {
|
|
15
|
+
if (typeof content === "string") {
|
|
16
|
+
return { text: content, images: [], imageCount: 0 };
|
|
17
|
+
}
|
|
18
|
+
const textParts: string[] = [];
|
|
19
|
+
const images: ImageContent[] = [];
|
|
20
|
+
for (const part of content) {
|
|
21
|
+
if (part.type === "text") textParts.push(part.text);
|
|
22
|
+
else images.push(part);
|
|
23
|
+
}
|
|
24
|
+
const text = textParts.join("\n");
|
|
25
|
+
return { text, images, imageCount: images.length };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Record a remotely/programmatically injected user message (e.g. Telegram
|
|
30
|
+
* inbound routed through the extension API) into the interactive TUI, so it is
|
|
31
|
+
* captured in prompt history and shown immediately instead of only appearing
|
|
32
|
+
* once the eventual `message_start` event lands.
|
|
33
|
+
*
|
|
34
|
+
* Local TUI submissions never reach this path (they go through
|
|
35
|
+
* `session.prompt(...)` / `startPendingSubmission`), so this cannot double-add
|
|
36
|
+
* local prompt history.
|
|
37
|
+
*
|
|
38
|
+
* - Always adds the injected text to editor prompt history.
|
|
39
|
+
* - Idle injections optimistically render the user message and record a pending
|
|
40
|
+
* injected optimistic signature (a counting Map, so multiple idle injections
|
|
41
|
+
* before the first `message_start` do not clobber each other); the later user
|
|
42
|
+
* `message_start` consumes one count and skips both the duplicate chat add and
|
|
43
|
+
* the defensive editor clear (so a locally typed draft is preserved).
|
|
44
|
+
* - Busy/queued injections refresh the pending-message display, which the
|
|
45
|
+
* caller has already populated by invoking `session.sendUserMessage(...)`
|
|
46
|
+
* before this helper.
|
|
47
|
+
*
|
|
48
|
+
* This helper never clears the editor text.
|
|
49
|
+
*/
|
|
50
|
+
export function applyInjectedUserSubmission(
|
|
51
|
+
ctx: InteractiveModeContext,
|
|
52
|
+
input: { content: string | (TextContent | ImageContent)[]; queued: boolean },
|
|
53
|
+
): void {
|
|
54
|
+
const { text, images, imageCount } = normalizeInjectedUserContent(input.content);
|
|
55
|
+
ctx.editor.addToHistory(text);
|
|
56
|
+
|
|
57
|
+
if (input.queued) {
|
|
58
|
+
ctx.updatePendingMessagesDisplay();
|
|
59
|
+
ctx.ui.requestRender();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
incrementInjectedOptimisticSignature(ctx, `${text}\u0000${imageCount}`);
|
|
64
|
+
ctx.addMessageToChat({
|
|
65
|
+
role: "user",
|
|
66
|
+
content: [{ type: "text", text }, ...images],
|
|
67
|
+
attribution: "user",
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
ctx.ui.requestRender();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Record one pending optimistic render for an injected user message.
|
|
75
|
+
*
|
|
76
|
+
* Injected sends are fire-and-forget and multiple idle injections can be
|
|
77
|
+
* rendered before the first `message_start` arrives, so a counting Map (not a
|
|
78
|
+
* single slot) tracks how many optimistic renders are outstanding per signature.
|
|
79
|
+
*/
|
|
80
|
+
export function incrementInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): void {
|
|
81
|
+
ctx.optimisticInjectedSignatures.set(signature, (ctx.optimisticInjectedSignatures.get(signature) ?? 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Consume one pending injected optimistic render for `signature`. Decrements the
|
|
86
|
+
* count (deleting the key at zero) and returns whether one was outstanding.
|
|
87
|
+
*/
|
|
88
|
+
export function consumeInjectedOptimisticSignature(ctx: InteractiveModeContext, signature: string): boolean {
|
|
89
|
+
const count = ctx.optimisticInjectedSignatures.get(signature) ?? 0;
|
|
90
|
+
if (count <= 0) return false;
|
|
91
|
+
if (count === 1) ctx.optimisticInjectedSignatures.delete(signature);
|
|
92
|
+
else ctx.optimisticInjectedSignatures.set(signature, count - 1);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
@@ -36,6 +36,19 @@ interface RenderInitialMessagesOptions {
|
|
|
36
36
|
preserveExistingChat?: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export function argsWithPartialJson(args: unknown, partialJson: unknown): unknown {
|
|
40
|
+
if (typeof partialJson !== "string" || !args || typeof args !== "object" || Array.isArray(args)) return args;
|
|
41
|
+
// Non-enumerable so the transient streaming buffer reaches renderers via direct
|
|
42
|
+
// property read but never serializes into persisted/wire message content.
|
|
43
|
+
Object.defineProperty(args, "__partialJson", {
|
|
44
|
+
value: partialJson,
|
|
45
|
+
enumerable: false,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
});
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
type QueuedMessages = {
|
|
40
53
|
steering: string[];
|
|
41
54
|
followUp: string[];
|
|
@@ -392,10 +405,10 @@ export class UiHelpers {
|
|
|
392
405
|
|
|
393
406
|
readGroup = null;
|
|
394
407
|
const tool = this.ctx.session.getToolByName(content.name);
|
|
395
|
-
const renderArgs =
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
const renderArgs = argsWithPartialJson(
|
|
409
|
+
content.arguments,
|
|
410
|
+
"partialJson" in content ? content.partialJson : undefined,
|
|
411
|
+
);
|
|
399
412
|
const component = new ToolExecutionComponent(
|
|
400
413
|
content.name,
|
|
401
414
|
renderArgs,
|
|
@@ -48,3 +48,23 @@ export function parseInThreadConfigCommand(text: string): ConfigCommandChange |
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a `/rich on|off` toggle. Returns `true`/`false` for a recognised
|
|
54
|
+
* on/off argument, or `undefined` otherwise (not a `/rich` command, or `/rich`
|
|
55
|
+
* with a missing/invalid argument). This is intentionally SEPARATE from
|
|
56
|
+
* `parseInThreadConfigCommand`: `/verbose`/`/redact` are producer/session config
|
|
57
|
+
* forwarded over the WS, whereas rich is Telegram-daemon delivery policy handled
|
|
58
|
+
* daemon-locally, so it never becomes a `config_command` frame or a user turn.
|
|
59
|
+
*/
|
|
60
|
+
export function parseRichToggleCommand(text: string): boolean | undefined {
|
|
61
|
+
const trimmed = text.trim();
|
|
62
|
+
if (!trimmed.startsWith("/")) return undefined;
|
|
63
|
+
const [rawCommand, ...rest] = trimmed.slice(1).split(/\s+/);
|
|
64
|
+
// Accept the "/rich@botname" form Telegram appends in group chats.
|
|
65
|
+
if (rawCommand?.toLowerCase().split("@")[0] !== "rich") return undefined;
|
|
66
|
+
const arg = rest[0]?.toLowerCase();
|
|
67
|
+
if (arg === "on" || arg === "true" || arg === "1") return true;
|
|
68
|
+
if (arg === "off" || arg === "false" || arg === "0") return false;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
@@ -16,6 +16,12 @@ export interface NotificationConfig {
|
|
|
16
16
|
redact: boolean;
|
|
17
17
|
verbosity: "lean" | "verbose";
|
|
18
18
|
idleTimeoutMs: number;
|
|
19
|
+
rich: {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
};
|
|
22
|
+
richDraft: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
};
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/** Read typed config from Settings. */
|
|
@@ -35,19 +41,50 @@ export function getNotificationConfig(settings: Settings): NotificationConfig {
|
|
|
35
41
|
redact: settings.get("notifications.redact"),
|
|
36
42
|
verbosity: settings.get("notifications.verbosity") === "verbose" ? "verbose" : "lean",
|
|
37
43
|
idleTimeoutMs: settings.get("notifications.daemon.idleTimeoutMs"),
|
|
44
|
+
rich: {
|
|
45
|
+
enabled: settings.get("notifications.telegram.rich.enabled"),
|
|
46
|
+
},
|
|
47
|
+
richDraft: {
|
|
48
|
+
enabled: settings.get("notifications.telegram.richDraft.enabled"),
|
|
49
|
+
},
|
|
38
50
|
};
|
|
39
51
|
}
|
|
40
52
|
|
|
53
|
+
export function hasNonBlankValue(value: string | undefined): boolean {
|
|
54
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Is Telegram configured with usable non-blank boundary credentials? */
|
|
58
|
+
export function isTelegramConfigured(
|
|
59
|
+
cfg: NotificationConfig,
|
|
60
|
+
): cfg is NotificationConfig & { botToken: string; chatId: string } {
|
|
61
|
+
return cfg.enabled && hasNonBlankValue(cfg.botToken) && hasNonBlankValue(cfg.chatId);
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/** Is global config sufficient for auto-on (enabled + at least one configured adapter)? */
|
|
42
65
|
export function isGloballyConfigured(cfg: NotificationConfig): boolean {
|
|
43
66
|
return (
|
|
44
67
|
cfg.enabled &&
|
|
45
|
-
((
|
|
46
|
-
(
|
|
47
|
-
(
|
|
68
|
+
(isTelegramConfigured(cfg) ||
|
|
69
|
+
(hasNonBlankValue(cfg.discord.botToken) && hasNonBlankValue(cfg.discord.channelId)) ||
|
|
70
|
+
(hasNonBlankValue(cfg.slack.botToken) && hasNonBlankValue(cfg.slack.channelId)))
|
|
48
71
|
);
|
|
49
72
|
}
|
|
50
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Per-run opt-out for completion notifications, honored before settings lookups.
|
|
76
|
+
*
|
|
77
|
+
* `GJC_NOTIFY=off` (also `0` / `false`, case-insensitive) suppresses the
|
|
78
|
+
* completion notification surface for this process only. `config.yml` is
|
|
79
|
+
* untouched and child processes inherit the env var, which lets non-interactive
|
|
80
|
+
* fleet runs (`gjc -p --no-session`) stay silent even when a user-level/global
|
|
81
|
+
* completion notification configuration is enabled.
|
|
82
|
+
*/
|
|
83
|
+
export function completionNotifyDisabledByEnv(env: NodeJS.ProcessEnv): boolean {
|
|
84
|
+
const v = env.GJC_NOTIFY?.trim().toLowerCase();
|
|
85
|
+
return v === "off" || v === "0" || v === "false";
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
/** Resolve whether the notifications extension should be registered at SDK startup. */
|
|
52
89
|
export function shouldRegisterNotificationsExtension(input: {
|
|
53
90
|
env: NodeJS.ProcessEnv;
|
|
@@ -60,6 +97,7 @@ export function shouldRegisterNotificationsExtension(input: {
|
|
|
60
97
|
currentAgentType?: string;
|
|
61
98
|
}): boolean {
|
|
62
99
|
if ((input.taskDepth ?? 0) > 0 || input.parentTaskPrefix || input.currentAgentType) return false;
|
|
100
|
+
if (completionNotifyDisabledByEnv(input.env)) return false;
|
|
63
101
|
if (input.env.GJC_NOTIFICATIONS === "0") return false;
|
|
64
102
|
if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
|
|
65
103
|
return input.cfg ? isGloballyConfigured(input.cfg) : false;
|
|
@@ -88,6 +126,7 @@ export function isSessionNotificationsEnabled(input: {
|
|
|
88
126
|
/** Mask a bot token for display: first 4 chars + "…" + "(len N)"; "(unset)" when undefined/empty. Never reveal full token. */
|
|
89
127
|
export function maskToken(token: string | undefined): string {
|
|
90
128
|
if (!token) return "(unset)";
|
|
129
|
+
if (token.length <= 4) return `…(len ${token.length})`;
|
|
91
130
|
return `${token.slice(0, 4)}…(len ${token.length})`;
|
|
92
131
|
}
|
|
93
132
|
|
|
@@ -131,6 +170,6 @@ export function buildRedactedAction(
|
|
|
131
170
|
// Asks stay fully readable/answerable even under redaction.
|
|
132
171
|
if (action.kind === "ask") return action;
|
|
133
172
|
|
|
134
|
-
const { summary: _summary, question: _question, ...base } = action;
|
|
173
|
+
const { summary: _summary, question: _question, options: _options, ...base } = action;
|
|
135
174
|
return base;
|
|
136
175
|
}
|
|
@@ -33,8 +33,8 @@ import { registerAskAnswerSource } from "../tools/ask-answer-registry";
|
|
|
33
33
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
34
34
|
import {
|
|
35
35
|
getNotificationConfig,
|
|
36
|
-
isGloballyConfigured,
|
|
37
36
|
isSessionNotificationsEnabled,
|
|
37
|
+
isTelegramConfigured,
|
|
38
38
|
type NotificationConfig,
|
|
39
39
|
sessionTag,
|
|
40
40
|
} from "./config";
|
|
@@ -90,6 +90,8 @@ export interface SessionCreateFrame {
|
|
|
90
90
|
target: SessionCreateTarget;
|
|
91
91
|
/** Reference to the daemon-written, once-consumed startup-prompt file. */
|
|
92
92
|
startupPromptRef?: string;
|
|
93
|
+
/** Model profile preset to activate for the spawned session (--mpreset). */
|
|
94
|
+
modelPreset?: string;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
/** Close (hard-kill, history preserved) a session. */
|
|
@@ -350,6 +352,15 @@ interface SessionRuntime {
|
|
|
350
352
|
/** Assistant text already flushed before an ask this turn (turn-scoped dedupe
|
|
351
353
|
* so turn_end does not re-emit the pre-ask lead-in). Reset each turn. */
|
|
352
354
|
preAskFlushedText?: string;
|
|
355
|
+
/** Live streaming: opt-in flag, monotonic per-turn ref, and emit throttle state. */
|
|
356
|
+
stream: boolean;
|
|
357
|
+
turnSeq?: number;
|
|
358
|
+
liveRef?: string;
|
|
359
|
+
lastLiveAt?: number;
|
|
360
|
+
lastLiveText?: string;
|
|
361
|
+
/** True between turn_end and the next turn_start: drops late async message_update
|
|
362
|
+
* frames so a stale live edit can never be emitted after the finalized turn. */
|
|
363
|
+
turnClosed?: boolean;
|
|
353
364
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
354
365
|
cancelPostmortemCleanup: () => void;
|
|
355
366
|
}
|
|
@@ -377,12 +388,41 @@ const defaultConfig: NotificationConfig = {
|
|
|
377
388
|
redact: false,
|
|
378
389
|
verbosity: "lean",
|
|
379
390
|
idleTimeoutMs: 60_000,
|
|
391
|
+
rich: { enabled: true },
|
|
392
|
+
richDraft: { enabled: false },
|
|
380
393
|
};
|
|
381
394
|
|
|
382
395
|
export function notificationsEnabled(): boolean {
|
|
383
396
|
return process.env.GJC_NOTIFICATIONS === "1" || Boolean(process.env.GJC_NOTIFICATIONS_TOKEN);
|
|
384
397
|
}
|
|
385
398
|
|
|
399
|
+
// Live streaming (opt-in): emit throttled non-finalized `turn_stream` frames as
|
|
400
|
+
// the assistant message streams so remote clients can edit ONE message live. The
|
|
401
|
+
// finalized frame (turn_end) carries the same messageRef and stays authoritative,
|
|
402
|
+
// so a dropped live frame self-heals. Off unless GJC_NOTIFICATIONS_STREAM=1.
|
|
403
|
+
function streamingEnabled(): boolean {
|
|
404
|
+
return process.env.GJC_NOTIFICATIONS_STREAM === "1";
|
|
405
|
+
}
|
|
406
|
+
function streamIntervalMs(): number {
|
|
407
|
+
return Math.max(200, Number(process.env.GJC_NOTIFICATIONS_STREAM_INTERVAL_MS) || 500);
|
|
408
|
+
}
|
|
409
|
+
// Max chars of a turn's assistant text carried by the FINALIZED turn_stream (and
|
|
410
|
+
// the pre-ask capture). Default 3500 keeps the mirror a glanceable per-turn
|
|
411
|
+
// summary; a client that splits long messages (the Telegram daemon does so via
|
|
412
|
+
// splitTelegramHtml, scheduling each chunk through the shared rate-limit pool so
|
|
413
|
+
// the fan-out never bypasses the per-chat limit) can raise it with
|
|
414
|
+
// GJC_NOTIFICATIONS_TURN_MAX to deliver full turns. The value is clamped to a
|
|
415
|
+
// finite [280, TURN_TEXT_MAX_CEILING] range: a non-finite or non-positive env
|
|
416
|
+
// (unset, NaN, Infinity, <= 0) falls back to the default, so the cap can never
|
|
417
|
+
// be unbounded. Live frames are intentionally NOT raised — they stay one
|
|
418
|
+
// editable preview message rather than fanning a long in-progress turn across
|
|
419
|
+
// sends.
|
|
420
|
+
const TURN_TEXT_MAX_CEILING = 40_000;
|
|
421
|
+
function turnTextMax(): number {
|
|
422
|
+
const raw = Number(process.env.GJC_NOTIFICATIONS_TURN_MAX);
|
|
423
|
+
if (!Number.isFinite(raw) || raw <= 0) return 3500;
|
|
424
|
+
return Math.min(TURN_TEXT_MAX_CEILING, Math.max(280, raw));
|
|
425
|
+
}
|
|
386
426
|
function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
|
|
387
427
|
if (settingsOverride)
|
|
388
428
|
return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
|
|
@@ -708,6 +748,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
708
748
|
cancelPostmortemCleanup: () => {},
|
|
709
749
|
redact,
|
|
710
750
|
verbosity,
|
|
751
|
+
stream: streamingEnabled(),
|
|
711
752
|
sessionTag: tag,
|
|
712
753
|
busy: false,
|
|
713
754
|
pendingInbound: new Set<number>(),
|
|
@@ -724,7 +765,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
724
765
|
});
|
|
725
766
|
logger.info(`notifications: serving session ${id} at ${endpoint.url} (unattended=${unattended})`);
|
|
726
767
|
|
|
727
|
-
if (settingsAvailable && settings &&
|
|
768
|
+
if (settingsAvailable && settings && isTelegramConfigured(cfg)) {
|
|
728
769
|
try {
|
|
729
770
|
await ensureTelegramDaemonRunning({ settings, cwd: ctx.cwd, sessionId: id });
|
|
730
771
|
} catch (e) {
|
|
@@ -963,7 +1004,10 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
963
1004
|
api.on("turn_start", (_event, ctx) => {
|
|
964
1005
|
const id = sessionId(ctx);
|
|
965
1006
|
const rt = runtimes.get(id);
|
|
966
|
-
if (!rt
|
|
1007
|
+
if (!rt) return;
|
|
1008
|
+
// A new turn is live: re-open the live-stream window (see turnClosed).
|
|
1009
|
+
rt.turnClosed = false;
|
|
1010
|
+
if (rt.pendingInbound.size === 0) return;
|
|
967
1011
|
for (const updateId of rt.pendingInbound) {
|
|
968
1012
|
try {
|
|
969
1013
|
rt.server.pushFrame(JSON.stringify({ type: "inbound_ack", sessionId: id, updateId, state: "consumed" }));
|
|
@@ -1059,11 +1103,29 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
1059
1103
|
// rate-limit pool before sending to Telegram.
|
|
1060
1104
|
// Push the in-flight turn's assistant text as a finalized turn_stream, deduped
|
|
1061
1105
|
// against what was already flushed for this turn (the pre-ask lead-in).
|
|
1062
|
-
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined): void => {
|
|
1106
|
+
const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
|
|
1063
1107
|
if (!text || text === rt.preAskFlushedText) return;
|
|
1064
1108
|
rt.preAskFlushedText = text;
|
|
1109
|
+
// Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
|
|
1110
|
+
// live message, never a fresh (rich-promotable) send. If live frames were
|
|
1111
|
+
// async-queued and none landed before this flush, allocate the per-turn ref
|
|
1112
|
+
// now so the finalized frame always carries a messageRef → the daemon keeps it
|
|
1113
|
+
// editable (HTML edit) and never rich-promotes a streamed final.
|
|
1114
|
+
if (finalAnswer && rt.stream && rt.liveRef === undefined) {
|
|
1115
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
1116
|
+
rt.liveRef = String(rt.turnSeq);
|
|
1117
|
+
}
|
|
1065
1118
|
try {
|
|
1066
|
-
rt.server.pushFrame(
|
|
1119
|
+
rt.server.pushFrame(
|
|
1120
|
+
JSON.stringify({
|
|
1121
|
+
type: "turn_stream",
|
|
1122
|
+
sessionId: id,
|
|
1123
|
+
phase: "finalized",
|
|
1124
|
+
finalAnswer,
|
|
1125
|
+
text,
|
|
1126
|
+
...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
|
|
1127
|
+
}),
|
|
1128
|
+
);
|
|
1067
1129
|
} catch (e) {
|
|
1068
1130
|
logger.warn(`notifications: pushFrame (turn) failed: ${String(e)}`);
|
|
1069
1131
|
}
|
|
@@ -1080,19 +1142,53 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
1080
1142
|
const id = sessionId(ctx);
|
|
1081
1143
|
const rt = runtimes.get(id);
|
|
1082
1144
|
if (!rt || rt.redact) return;
|
|
1083
|
-
flushTurnText(rt, id, rt.currentTurnText);
|
|
1145
|
+
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
1084
1146
|
});
|
|
1085
1147
|
|
|
1086
1148
|
api.on("turn_end", (event, ctx) => {
|
|
1087
1149
|
const id = sessionId(ctx);
|
|
1088
1150
|
const rt = runtimes.get(id);
|
|
1089
1151
|
if (!rt) return;
|
|
1090
|
-
const text = rt.redact ? undefined : summaryFromMessage(event.message,
|
|
1091
|
-
if (text) flushTurnText(rt, id, text);
|
|
1152
|
+
const text = rt.redact ? undefined : summaryFromMessage(event.message, turnTextMax());
|
|
1153
|
+
if (text) flushTurnText(rt, id, text, true);
|
|
1092
1154
|
// Reset per-turn streaming state so the next turn starts fresh and a later
|
|
1093
1155
|
// turn with identical text is not falsely deduped.
|
|
1094
1156
|
rt.currentTurnText = undefined;
|
|
1095
1157
|
rt.preAskFlushedText = undefined;
|
|
1158
|
+
rt.liveRef = undefined;
|
|
1159
|
+
// Close the live-stream window: any message_update queued after turn_end is
|
|
1160
|
+
// dropped so it can never emit a stale live edit past the finalized turn.
|
|
1161
|
+
rt.turnClosed = true;
|
|
1162
|
+
rt.lastLiveAt = undefined;
|
|
1163
|
+
rt.lastLiveText = undefined;
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
// Live streaming (opt-in): push throttled in-progress assistant text as
|
|
1167
|
+
// non-finalized turn_stream frames so remote clients edit one message as the
|
|
1168
|
+
// turn streams. The finalized frame (turn_end) carries the same messageRef and
|
|
1169
|
+
// lands the authoritative text. Suppressed under redaction.
|
|
1170
|
+
api.on("message_update", (event, ctx) => {
|
|
1171
|
+
const id = sessionId(ctx);
|
|
1172
|
+
const rt = runtimes.get(id);
|
|
1173
|
+
if (!rt?.stream || rt.redact || rt.turnClosed) return;
|
|
1174
|
+
if ((event.message as { role?: unknown }).role !== "assistant") return;
|
|
1175
|
+
if (rt.liveRef === undefined) {
|
|
1176
|
+
rt.turnSeq = (rt.turnSeq ?? 0) + 1;
|
|
1177
|
+
rt.liveRef = String(rt.turnSeq);
|
|
1178
|
+
}
|
|
1179
|
+
const now = Date.now();
|
|
1180
|
+
if (now - (rt.lastLiveAt ?? 0) < streamIntervalMs()) return;
|
|
1181
|
+
const text = summaryFromMessage(event.message, 3500);
|
|
1182
|
+
if (!text || text === rt.lastLiveText) return;
|
|
1183
|
+
rt.lastLiveAt = now;
|
|
1184
|
+
rt.lastLiveText = text;
|
|
1185
|
+
try {
|
|
1186
|
+
rt.server.pushFrame(
|
|
1187
|
+
JSON.stringify({ type: "turn_stream", sessionId: id, phase: "live", text, messageRef: rt.liveRef }),
|
|
1188
|
+
);
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
logger.warn(`notifications: pushFrame (live) failed: ${String(e)}`);
|
|
1191
|
+
}
|
|
1096
1192
|
});
|
|
1097
1193
|
|
|
1098
1194
|
// Stream agent-produced images (computer/browser/tool screenshots) as
|
|
@@ -1106,7 +1202,7 @@ export function createNotificationsExtension(api: ExtensionAPI, options: { setti
|
|
|
1106
1202
|
// flush can emit it before the ask prompt. Role-scoped: message_end also
|
|
1107
1203
|
// fires for the user prompt, which must never be mirrored back as turn output.
|
|
1108
1204
|
if ((event.message as { role?: unknown }).role === "assistant") {
|
|
1109
|
-
const turnText = summaryFromMessage(event.message,
|
|
1205
|
+
const turnText = summaryFromMessage(event.message, turnTextMax());
|
|
1110
1206
|
if (turnText) rt.currentTurnText = turnText;
|
|
1111
1207
|
}
|
|
1112
1208
|
for (const img of imageAttachmentsFromMessage(event.message)) {
|
|
@@ -31,7 +31,7 @@ function normalizeLifecycleCommandToken(
|
|
|
31
31
|
|
|
32
32
|
/** A parsed, validated lifecycle command (transport identity added by caller). */
|
|
33
33
|
export type ParsedLifecycleCommand =
|
|
34
|
-
| { kind: "create"; target: SessionCreateTarget }
|
|
34
|
+
| { kind: "create"; target: SessionCreateTarget; modelPreset?: string }
|
|
35
35
|
| { kind: "close"; target: SessionCloseTarget }
|
|
36
36
|
| { kind: "resume"; target: SessionResumeTarget }
|
|
37
37
|
| { kind: "recent"; which: "create" | "resume" | "all" }
|
|
@@ -41,9 +41,9 @@ export type ParsedLifecycleCommand =
|
|
|
41
41
|
|
|
42
42
|
const USAGE = [
|
|
43
43
|
"Session commands:",
|
|
44
|
-
"/session_create path <dir>",
|
|
45
|
-
"/session_create worktree <repo> <branch>",
|
|
46
|
-
"/session_create dir <newdir>",
|
|
44
|
+
"/session_create path <dir> [--mpreset <profile>]",
|
|
45
|
+
"/session_create worktree <repo> <branch> [--mpreset <profile>]",
|
|
46
|
+
"/session_create dir <newdir> [--mpreset <profile>]",
|
|
47
47
|
"/session_close <sessionId>",
|
|
48
48
|
"/session_resume <sessionId|prefix>",
|
|
49
49
|
"/session_recent [create|resume]",
|
|
@@ -69,6 +69,23 @@ export function isLifecycleCommandText(
|
|
|
69
69
|
return normalizeLifecycleCommandToken(rawCommand ?? "", ctx) !== undefined;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Extract `--mpreset <name>` (or `--mpreset=<name>`) from args, returning remaining positional args. */
|
|
73
|
+
function extractModelPreset(args: string[]): { positional: string[]; modelPreset?: string } {
|
|
74
|
+
const positional: string[] = [];
|
|
75
|
+
let modelPreset: string | undefined;
|
|
76
|
+
for (let i = 0; i < args.length; i++) {
|
|
77
|
+
const arg = args[i]!;
|
|
78
|
+
if (arg === "--mpreset" && i + 1 < args.length) {
|
|
79
|
+
modelPreset = args[++i]!;
|
|
80
|
+
} else if (arg.startsWith("--mpreset=")) {
|
|
81
|
+
modelPreset = arg.slice("--mpreset=".length);
|
|
82
|
+
} else {
|
|
83
|
+
positional.push(arg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { positional, modelPreset };
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
/**
|
|
73
90
|
* Parse a paired-chat message into a lifecycle command. Returns `none` for
|
|
74
91
|
* non-lifecycle text, `usage`/`reject` for malformed input (no side effect), or
|
|
@@ -121,29 +138,33 @@ export function parseLifecycleCommand(
|
|
|
121
138
|
return { kind: "resume", target: { sessionIdOrPrefix: idOrPrefix } };
|
|
122
139
|
}
|
|
123
140
|
|
|
124
|
-
// /session_create <kind> ...
|
|
125
|
-
const
|
|
141
|
+
// /session_create <kind> ... [--mpreset <profile>]
|
|
142
|
+
const { positional, modelPreset } = extractModelPreset(args);
|
|
143
|
+
if (modelPreset !== undefined && !isSafeIdentifier(modelPreset)) {
|
|
144
|
+
return { kind: "reject", reason: "invalid_target", message: `Invalid model preset name.\n\n${USAGE}` };
|
|
145
|
+
}
|
|
146
|
+
const kind = positional[0];
|
|
126
147
|
if (kind === "path") {
|
|
127
|
-
if (
|
|
128
|
-
const p = normalizeLifecyclePath(
|
|
148
|
+
if (positional.length !== 2) return { kind: "usage", message: USAGE };
|
|
149
|
+
const p = normalizeLifecyclePath(positional[1]!);
|
|
129
150
|
if (!p) return { kind: "reject", reason: "invalid_target", message: `Invalid path.\n\n${USAGE}` };
|
|
130
|
-
return { kind: "create", target: { kind: "existing_path", path: p } };
|
|
151
|
+
return { kind: "create", target: { kind: "existing_path", path: p }, modelPreset };
|
|
131
152
|
}
|
|
132
153
|
if (kind === "dir") {
|
|
133
|
-
if (
|
|
134
|
-
const p = normalizeLifecyclePath(
|
|
154
|
+
if (positional.length !== 2) return { kind: "usage", message: USAGE };
|
|
155
|
+
const p = normalizeLifecyclePath(positional[1]!);
|
|
135
156
|
if (!p) return { kind: "reject", reason: "invalid_target", message: `Invalid dir.\n\n${USAGE}` };
|
|
136
|
-
return { kind: "create", target: { kind: "plain_dir", path: p } };
|
|
157
|
+
return { kind: "create", target: { kind: "plain_dir", path: p }, modelPreset };
|
|
137
158
|
}
|
|
138
159
|
if (kind === "worktree") {
|
|
139
|
-
if (
|
|
140
|
-
const repo = normalizeLifecyclePath(
|
|
141
|
-
const branch =
|
|
160
|
+
if (positional.length !== 3) return { kind: "usage", message: USAGE };
|
|
161
|
+
const repo = normalizeLifecyclePath(positional[1]!);
|
|
162
|
+
const branch = positional[2]!;
|
|
142
163
|
if (!repo) return { kind: "reject", reason: "invalid_target", message: `Invalid repo path.\n\n${USAGE}` };
|
|
143
164
|
if (!isSafeBranch(branch)) {
|
|
144
165
|
return { kind: "reject", reason: "invalid_target", message: `Invalid branch name.\n\n${USAGE}` };
|
|
145
166
|
}
|
|
146
|
-
return { kind: "create", target: { kind: "worktree", repo, branch } };
|
|
167
|
+
return { kind: "create", target: { kind: "worktree", repo, branch }, modelPreset };
|
|
147
168
|
}
|
|
148
169
|
return { kind: "usage", message: USAGE };
|
|
149
170
|
}
|