@sayknow-cli/coding-agent 0.3.10 → 0.3.12
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 +0 -13
- package/dist/types/build-metadata.d.ts +7 -0
- package/dist/types/capability/mcp.d.ts +3 -1
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +6 -0
- package/dist/types/modes/components/status-line.d.ts +1 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
- package/dist/types/notifications/telegram-daemon.d.ts +2 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
- package/dist/types/runtime-mcp/types.d.ts +4 -3
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/tools/skill.d.ts +9 -8
- package/dist/types/utils/linkedom.d.ts +4 -0
- package/dist/types/utils/pasted-image-path.d.ts +5 -3
- package/package.json +7 -7
- package/scripts/build-binary.ts +2 -43
- package/scripts/compile-args.ts +106 -0
- package/scripts/generate-docs-index.ts +1 -0
- package/src/build-metadata.ts +104 -0
- package/src/capability/mcp.ts +3 -1
- package/src/cli/mcp-cli.ts +108 -26
- package/src/cli.ts +21 -0
- package/src/commands/mcp.ts +8 -8
- package/src/commands/stats.ts +6 -2
- package/src/config/keybindings.ts +2 -2
- package/src/config/mcp-schema.json +21 -17
- package/src/config/model-profiles.ts +23 -23
- package/src/discovery/builtin.ts +14 -0
- package/src/discovery/mcp-json.ts +14 -0
- package/src/eval/js/executor.ts +7 -1
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/skc-protocol.ts +14 -1
- package/src/modes/acp/acp-agent.ts +72 -16
- package/src/modes/acp/acp-client-bridge.ts +27 -5
- package/src/modes/acp/acp-event-mapper.ts +39 -1
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/hook-selector.ts +4 -0
- package/src/modes/components/model-selector.ts +17 -6
- package/src/modes/components/session-selector.ts +2 -2
- package/src/modes/components/status-line/presets.ts +5 -19
- package/src/modes/components/status-line/segments.ts +14 -0
- package/src/modes/components/status-line/types.ts +6 -0
- package/src/modes/components/status-line.ts +13 -6
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +9 -9
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/controllers/selector-controller.ts +7 -3
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/rpc/rpc-client.ts +30 -6
- package/src/modes/utils/ui-helpers.ts +154 -32
- package/src/notifications/telegram-daemon.ts +28 -2
- package/src/prompts/tools/skill.md +2 -2
- package/src/runtime-mcp/config-writer.ts +4 -3
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/types.ts +4 -3
- package/src/session/agent-session.ts +117 -3
- package/src/session/session-manager.ts +68 -40
- package/src/skc-runtime/state-runtime.ts +105 -12
- package/src/skc-runtime/workflow-manifest.generated.json +4 -28
- package/src/skc-runtime/workflow-manifest.ts +1 -7
- package/src/slash-commands/builtin-registry.ts +9 -4
- package/src/tools/bash-interactive.ts +9 -4
- package/src/tools/browser/readable.ts +2 -2
- package/src/tools/fetch.ts +5 -5
- package/src/tools/skill.ts +14 -11
- package/src/utils/linkedom.ts +14 -0
- package/src/utils/pasted-image-path.ts +23 -9
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +2 -2
- package/src/web/scrapers/twitter.ts +3 -2
- package/src/web/scrapers/wikipedia.ts +2 -2
|
@@ -8,7 +8,8 @@ import type { CompactionResult } from "@sayknow-cli/agent-core/compaction";
|
|
|
8
8
|
import type { ImageContent, Model } from "@sayknow-cli/ai";
|
|
9
9
|
import { isRecord, ptree, readJsonl } from "@sayknow-cli/utils";
|
|
10
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
11
|
-
import type { SessionStats } from "../../session/agent-session";
|
|
11
|
+
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
|
12
|
+
import { AGENT_WIRE_EVENT_TYPES, type AgentWireEventType } from "../shared/agent-wire/event-contract";
|
|
12
13
|
import type {
|
|
13
14
|
RpcCommand,
|
|
14
15
|
RpcExtensionUIRequest,
|
|
@@ -64,6 +65,7 @@ export interface RpcClientOptions {
|
|
|
64
65
|
export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
|
|
65
66
|
|
|
66
67
|
export type RpcEventListener = (event: AgentEvent) => void;
|
|
68
|
+
export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
|
|
67
69
|
|
|
68
70
|
export interface RpcClientToolContext<TDetails = unknown> {
|
|
69
71
|
toolCallId: string;
|
|
@@ -91,7 +93,8 @@ export function defineRpcClientTool<
|
|
|
91
93
|
return tool;
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
const
|
|
96
|
+
const agentWireEventTypes = new Set<AgentWireEventType>(AGENT_WIRE_EVENT_TYPES);
|
|
97
|
+
const coreAgentEventTypes = new Set<AgentEvent["type"]>([
|
|
95
98
|
"agent_start",
|
|
96
99
|
"agent_end",
|
|
97
100
|
"turn_start",
|
|
@@ -116,13 +119,16 @@ function isRpcResponse(value: unknown): value is RpcResponse {
|
|
|
116
119
|
return true;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
function
|
|
122
|
+
function isAgentSessionEvent(value: unknown): value is AgentSessionEvent {
|
|
120
123
|
if (!isRecord(value)) return false;
|
|
121
124
|
const type = value.type;
|
|
122
125
|
if (typeof type !== "string") return false;
|
|
123
|
-
return
|
|
126
|
+
return agentWireEventTypes.has(type as AgentWireEventType);
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
function isCoreAgentEvent(value: AgentSessionEvent): value is AgentEvent {
|
|
130
|
+
return coreAgentEventTypes.has(value.type as AgentEvent["type"]);
|
|
131
|
+
}
|
|
126
132
|
function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
|
|
127
133
|
if (!isRecord(value)) return false;
|
|
128
134
|
return (
|
|
@@ -378,6 +384,7 @@ export class RpcClient {
|
|
|
378
384
|
#transport: RpcTransport | null = null;
|
|
379
385
|
#transportClosed = false;
|
|
380
386
|
#eventListeners: RpcEventListener[] = [];
|
|
387
|
+
#sessionEventListeners: RpcSessionEventListener[] = [];
|
|
381
388
|
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
382
389
|
new Map();
|
|
383
390
|
#customTools: RpcClientCustomTool[] = [];
|
|
@@ -442,7 +449,7 @@ export class RpcClient {
|
|
|
442
449
|
}
|
|
443
450
|
|
|
444
451
|
/**
|
|
445
|
-
* Subscribe to
|
|
452
|
+
* Subscribe to the legacy core AgentEvent subset.
|
|
446
453
|
*/
|
|
447
454
|
onEvent(listener: RpcEventListener): () => void {
|
|
448
455
|
this.#eventListeners.push(listener);
|
|
@@ -454,6 +461,19 @@ export class RpcClient {
|
|
|
454
461
|
};
|
|
455
462
|
}
|
|
456
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Subscribe to all renderer-facing agent session events.
|
|
466
|
+
*/
|
|
467
|
+
onSessionEvent(listener: RpcSessionEventListener): () => void {
|
|
468
|
+
this.#sessionEventListeners.push(listener);
|
|
469
|
+
return () => {
|
|
470
|
+
const index = this.#sessionEventListeners.indexOf(listener);
|
|
471
|
+
if (index !== -1) {
|
|
472
|
+
this.#sessionEventListeners.splice(index, 1);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
457
477
|
/**
|
|
458
478
|
* Subscribe to workflow lifecycle gates emitted by RPC mode.
|
|
459
479
|
*/
|
|
@@ -928,8 +948,12 @@ export class RpcClient {
|
|
|
928
948
|
|
|
929
949
|
// Canonical agent-wire event frame: { type:"event", payload:{ event_type, event } }.
|
|
930
950
|
const event = unwrapAgentWireEventFrame(data);
|
|
931
|
-
if (!
|
|
951
|
+
if (!isAgentSessionEvent(event)) return;
|
|
932
952
|
|
|
953
|
+
for (const listener of this.#sessionEventListeners) {
|
|
954
|
+
listener(event);
|
|
955
|
+
}
|
|
956
|
+
if (!isCoreAgentEvent(event)) return;
|
|
933
957
|
for (const listener of this.#eventListeners) {
|
|
934
958
|
listener(event);
|
|
935
959
|
}
|
|
@@ -65,6 +65,127 @@ type QueuedMessages = {
|
|
|
65
65
|
followUp: string[];
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
+
const CHAT_CHILD_CAP = 400;
|
|
69
|
+
const CHAT_COLLAPSE_BATCH_SIZE = 300;
|
|
70
|
+
|
|
71
|
+
const chatChildAddedAt = new WeakMap<Component, number>();
|
|
72
|
+
|
|
73
|
+
export class CollapsedChatHistoryComponent implements Component {
|
|
74
|
+
readonly isCollapsedChatHistory = true;
|
|
75
|
+
count: number;
|
|
76
|
+
startTime: number;
|
|
77
|
+
endTime: number;
|
|
78
|
+
|
|
79
|
+
constructor(count: number, startTime: number, endTime: number) {
|
|
80
|
+
this.count = count;
|
|
81
|
+
this.startTime = startTime;
|
|
82
|
+
this.endTime = endTime;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
merge(count: number, startTime: number, endTime: number): void {
|
|
86
|
+
this.count += count;
|
|
87
|
+
this.startTime = Math.min(this.startTime, startTime);
|
|
88
|
+
this.endTime = Math.max(this.endTime, endTime);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
render(_width: number): string[] {
|
|
92
|
+
const label = `[${this.count} earlier messages collapsed — ${formatChatCollapseTime(this.startTime)} to ${formatChatCollapseTime(this.endTime)}]`;
|
|
93
|
+
return [theme.fg("dim", label)];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
invalidate(): void {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatChatCollapseTime(timestamp: number): string {
|
|
100
|
+
return new Date(timestamp).toLocaleTimeString(undefined, {
|
|
101
|
+
hour: "2-digit",
|
|
102
|
+
minute: "2-digit",
|
|
103
|
+
hour12: false,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCollapsedChatHistory(component: Component): component is CollapsedChatHistoryComponent {
|
|
108
|
+
return component instanceof CollapsedChatHistoryComponent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isActiveChatChild(ctx: InteractiveModeContext, component: Component): boolean {
|
|
112
|
+
if ([...ctx.pendingTools.values()].includes(component as never)) return true;
|
|
113
|
+
if (ctx.pendingBashComponents.includes(component as never)) return true;
|
|
114
|
+
if (ctx.pendingPythonComponents.includes(component as never)) return true;
|
|
115
|
+
return component === ctx.bashComponent || component === ctx.pythonComponent || component === ctx.streamingComponent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getChatChildTime(component: Component): number {
|
|
119
|
+
return chatChildAddedAt.get(component) ?? Date.now();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function addChatChild(ctx: InteractiveModeContext, component: Component): void {
|
|
123
|
+
ctx.chatContainer.addChild(component);
|
|
124
|
+
chatChildAddedAt.set(component, Date.now());
|
|
125
|
+
trimChatChildren(ctx);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function trimChatChildren(ctx: InteractiveModeContext): void {
|
|
129
|
+
const children = ctx.chatContainer.children;
|
|
130
|
+
|
|
131
|
+
// Collapse the oldest completed children into history placeholders until the cap is
|
|
132
|
+
// satisfied or no further reduction is structurally possible. Active (pending/running)
|
|
133
|
+
// children and existing placeholders are never collapsed, so a conversation whose
|
|
134
|
+
// top-level children are dominated by active components may legitimately exceed the cap.
|
|
135
|
+
let scanFrom = 0;
|
|
136
|
+
while (children.length > CHAT_CHILD_CAP) {
|
|
137
|
+
// Advance to the oldest collapsible completed child, skipping active children and
|
|
138
|
+
// existing placeholders so the cap is enforced even when the oldest child is active.
|
|
139
|
+
while (
|
|
140
|
+
scanFrom < children.length &&
|
|
141
|
+
(isActiveChatChild(ctx, children[scanFrom]) || isCollapsedChatHistory(children[scanFrom]))
|
|
142
|
+
) {
|
|
143
|
+
scanFrom++;
|
|
144
|
+
}
|
|
145
|
+
if (scanFrom >= children.length) return; // only active/placeholder children remain
|
|
146
|
+
|
|
147
|
+
// Extend over the oldest contiguous run of completed children (up to the batch size).
|
|
148
|
+
let collapseEndExclusive = scanFrom;
|
|
149
|
+
const maxEnd = Math.min(children.length, scanFrom + CHAT_COLLAPSE_BATCH_SIZE);
|
|
150
|
+
while (
|
|
151
|
+
collapseEndExclusive < maxEnd &&
|
|
152
|
+
!isActiveChatChild(ctx, children[collapseEndExclusive]) &&
|
|
153
|
+
!isCollapsedChatHistory(children[collapseEndExclusive])
|
|
154
|
+
) {
|
|
155
|
+
collapseEndExclusive++;
|
|
156
|
+
}
|
|
157
|
+
const collapseCount = collapseEndExclusive - scanFrom;
|
|
158
|
+
|
|
159
|
+
const prev = scanFrom > 0 ? children[scanFrom - 1] : undefined;
|
|
160
|
+
const mergeTarget = prev && isCollapsedChatHistory(prev) ? prev : undefined;
|
|
161
|
+
// A lone completed child with no adjacent placeholder to merge into cannot reduce
|
|
162
|
+
// the child count (collapsing it yields a 1-count placeholder). Skip past it.
|
|
163
|
+
if (!mergeTarget && collapseCount <= 1) {
|
|
164
|
+
scanFrom += Math.max(collapseCount, 1);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const collapsed = children.slice(scanFrom, collapseEndExclusive);
|
|
169
|
+
const startTime = getChatChildTime(collapsed[0]);
|
|
170
|
+
const endTime = getChatChildTime(collapsed[collapsed.length - 1]);
|
|
171
|
+
for (const child of collapsed) {
|
|
172
|
+
child.dispose?.();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mergeTarget) {
|
|
176
|
+
mergeTarget.merge(collapseCount, startTime, endTime);
|
|
177
|
+
children.splice(scanFrom, collapseCount);
|
|
178
|
+
chatChildAddedAt.set(mergeTarget, mergeTarget.startTime);
|
|
179
|
+
// scanFrom stays; whatever follows the merged run is next.
|
|
180
|
+
} else {
|
|
181
|
+
const placeholder = new CollapsedChatHistoryComponent(collapseCount, startTime, endTime);
|
|
182
|
+
children.splice(scanFrom, collapseCount, placeholder);
|
|
183
|
+
chatChildAddedAt.set(placeholder, startTime);
|
|
184
|
+
scanFrom++; // step past the freshly created placeholder
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
68
189
|
export class UiHelpers {
|
|
69
190
|
constructor(private ctx: InteractiveModeContext) {}
|
|
70
191
|
|
|
@@ -103,8 +224,8 @@ export class UiHelpers {
|
|
|
103
224
|
|
|
104
225
|
const spacer = new Spacer(1);
|
|
105
226
|
const text = new Text(rendered, 1, 0);
|
|
106
|
-
this.ctx
|
|
107
|
-
this.ctx
|
|
227
|
+
addChatChild(this.ctx, spacer);
|
|
228
|
+
addChatChild(this.ctx, text);
|
|
108
229
|
this.ctx.lastStatusSpacer = spacer;
|
|
109
230
|
this.ctx.lastStatusText = text;
|
|
110
231
|
this.ctx.ui.requestRender();
|
|
@@ -120,7 +241,7 @@ export class UiHelpers {
|
|
|
120
241
|
component.setComplete(message.exitCode, message.cancelled, {
|
|
121
242
|
truncation: message.meta?.truncation,
|
|
122
243
|
});
|
|
123
|
-
this.ctx
|
|
244
|
+
addChatChild(this.ctx, component);
|
|
124
245
|
break;
|
|
125
246
|
}
|
|
126
247
|
case "pythonExecution": {
|
|
@@ -131,7 +252,7 @@ export class UiHelpers {
|
|
|
131
252
|
component.setComplete(message.exitCode, message.cancelled, {
|
|
132
253
|
truncation: message.meta?.truncation,
|
|
133
254
|
});
|
|
134
|
-
this.ctx
|
|
255
|
+
addChatChild(this.ctx, component);
|
|
135
256
|
break;
|
|
136
257
|
}
|
|
137
258
|
case "hookMessage":
|
|
@@ -175,14 +296,14 @@ export class UiHelpers {
|
|
|
175
296
|
]
|
|
176
297
|
.filter(Boolean)
|
|
177
298
|
.join(" ");
|
|
178
|
-
this.ctx
|
|
299
|
+
addChatChild(this.ctx, new Text(line, 1, 0));
|
|
179
300
|
}
|
|
180
301
|
break;
|
|
181
302
|
}
|
|
182
303
|
if (message.customType === SKILL_PROMPT_MESSAGE_TYPE) {
|
|
183
304
|
const component = new SkillMessageComponent(message as CustomMessage<SkillPromptDetails>);
|
|
184
305
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
185
|
-
this.ctx
|
|
306
|
+
addChatChild(this.ctx, component);
|
|
186
307
|
break;
|
|
187
308
|
}
|
|
188
309
|
if (
|
|
@@ -219,12 +340,12 @@ export class UiHelpers {
|
|
|
219
340
|
const components: Component[] = [];
|
|
220
341
|
const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
|
|
221
342
|
const headerComponent = new Text(header, 1, 0);
|
|
222
|
-
this.ctx
|
|
343
|
+
addChatChild(this.ctx, headerComponent);
|
|
223
344
|
components.push(headerComponent);
|
|
224
345
|
if (body) {
|
|
225
346
|
for (const line of body.split("\n")) {
|
|
226
347
|
const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
|
|
227
|
-
this.ctx
|
|
348
|
+
addChatChild(this.ctx, lineComponent);
|
|
228
349
|
components.push(lineComponent);
|
|
229
350
|
}
|
|
230
351
|
}
|
|
@@ -242,12 +363,12 @@ export class UiHelpers {
|
|
|
242
363
|
const components: Component[] = [];
|
|
243
364
|
const header = `${theme.fg("accent", `[Steer ${details?.state ?? "queued"}] ${details?.from ?? "?"} ⇨ ${details?.to ?? "?"}`)}`;
|
|
244
365
|
const headerComponent = new Text(header, 1, 0);
|
|
245
|
-
this.ctx
|
|
366
|
+
addChatChild(this.ctx, headerComponent);
|
|
246
367
|
components.push(headerComponent);
|
|
247
368
|
if (details?.body) {
|
|
248
369
|
for (const line of details.body.split("\n")) {
|
|
249
370
|
const lineComponent = new Text(theme.fg("muted", ` ${line}`), 0, 0);
|
|
250
|
-
this.ctx
|
|
371
|
+
addChatChild(this.ctx, lineComponent);
|
|
251
372
|
components.push(lineComponent);
|
|
252
373
|
}
|
|
253
374
|
}
|
|
@@ -257,22 +378,22 @@ export class UiHelpers {
|
|
|
257
378
|
// Both HookMessage and CustomMessage have the same structure, cast for compatibility
|
|
258
379
|
const component = new CustomMessageComponent(message as CustomMessage<unknown>, renderer);
|
|
259
380
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
260
|
-
this.ctx
|
|
381
|
+
addChatChild(this.ctx, component);
|
|
261
382
|
}
|
|
262
383
|
break;
|
|
263
384
|
}
|
|
264
385
|
case "compactionSummary": {
|
|
265
|
-
this.ctx
|
|
386
|
+
addChatChild(this.ctx, new Spacer(1));
|
|
266
387
|
const component = new CompactionSummaryMessageComponent(message);
|
|
267
388
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
268
|
-
this.ctx
|
|
389
|
+
addChatChild(this.ctx, component);
|
|
269
390
|
break;
|
|
270
391
|
}
|
|
271
392
|
case "branchSummary": {
|
|
272
|
-
this.ctx
|
|
393
|
+
addChatChild(this.ctx, new Spacer(1));
|
|
273
394
|
const component = new BranchSummaryMessageComponent(message);
|
|
274
395
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
275
|
-
this.ctx
|
|
396
|
+
addChatChild(this.ctx, component);
|
|
276
397
|
break;
|
|
277
398
|
}
|
|
278
399
|
case "fileMention": {
|
|
@@ -293,7 +414,7 @@ export class UiHelpers {
|
|
|
293
414
|
"accent",
|
|
294
415
|
file.path,
|
|
295
416
|
)} ${theme.fg("dim", suffix)}`;
|
|
296
|
-
this.ctx
|
|
417
|
+
addChatChild(this.ctx, new Text(text, 0, 0));
|
|
297
418
|
}
|
|
298
419
|
break;
|
|
299
420
|
}
|
|
@@ -303,7 +424,7 @@ export class UiHelpers {
|
|
|
303
424
|
if (textContent) {
|
|
304
425
|
const isSynthetic = message.role === "developer" ? true : (message.synthetic ?? false);
|
|
305
426
|
const userComponent = new UserMessageComponent(textContent, isSynthetic);
|
|
306
|
-
this.ctx
|
|
427
|
+
addChatChild(this.ctx, userComponent);
|
|
307
428
|
if (options?.populateHistory && message.role === "user" && !isSynthetic) {
|
|
308
429
|
this.ctx.editor.addToHistory(textContent);
|
|
309
430
|
}
|
|
@@ -314,7 +435,7 @@ export class UiHelpers {
|
|
|
314
435
|
const assistantComponent = new AssistantMessageComponent(message, this.ctx.hideThinkingBlock, () =>
|
|
315
436
|
this.ctx.ui.requestRender(),
|
|
316
437
|
);
|
|
317
|
-
this.ctx
|
|
438
|
+
addChatChild(this.ctx, assistantComponent);
|
|
318
439
|
break;
|
|
319
440
|
}
|
|
320
441
|
case "toolResult": {
|
|
@@ -394,7 +515,7 @@ export class UiHelpers {
|
|
|
394
515
|
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
|
395
516
|
});
|
|
396
517
|
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
397
|
-
this.ctx
|
|
518
|
+
addChatChild(this.ctx, readGroup);
|
|
398
519
|
}
|
|
399
520
|
readGroup.updateArgs(content.arguments, content.id);
|
|
400
521
|
readGroup.updateResult(
|
|
@@ -436,7 +557,7 @@ export class UiHelpers {
|
|
|
436
557
|
content.id,
|
|
437
558
|
);
|
|
438
559
|
component.setExpanded(this.ctx.toolOutputExpanded);
|
|
439
|
-
this.ctx
|
|
560
|
+
addChatChild(this.ctx, component);
|
|
440
561
|
|
|
441
562
|
if (hasErrorStop && errorMessage) {
|
|
442
563
|
component.updateResult(
|
|
@@ -474,7 +595,7 @@ export class UiHelpers {
|
|
|
474
595
|
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
|
475
596
|
});
|
|
476
597
|
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
477
|
-
this.ctx
|
|
598
|
+
addChatChild(this.ctx, readGroup);
|
|
478
599
|
}
|
|
479
600
|
const args = readToolCallArgs.get(message.toolCallId);
|
|
480
601
|
if (args) {
|
|
@@ -541,7 +662,7 @@ export class UiHelpers {
|
|
|
541
662
|
}
|
|
542
663
|
if (preservedChatChildren && preservedChatChildren.length > 0) {
|
|
543
664
|
for (const child of preservedChatChildren) {
|
|
544
|
-
this.ctx
|
|
665
|
+
addChatChild(this.ctx, child);
|
|
545
666
|
}
|
|
546
667
|
this.ctx.ui.requestRender();
|
|
547
668
|
}
|
|
@@ -563,8 +684,8 @@ export class UiHelpers {
|
|
|
563
684
|
process.stderr.write(`${label}: ${errorMessage}\n`);
|
|
564
685
|
return;
|
|
565
686
|
}
|
|
566
|
-
this.ctx
|
|
567
|
-
this.ctx
|
|
687
|
+
addChatChild(this.ctx, new Spacer(1));
|
|
688
|
+
addChatChild(this.ctx, new Text(theme.fg("error", `${label}: ${errorMessage}`), 1, 0));
|
|
568
689
|
this.ctx.ui.requestRender();
|
|
569
690
|
}
|
|
570
691
|
|
|
@@ -575,15 +696,16 @@ export class UiHelpers {
|
|
|
575
696
|
process.stderr.write(`${label}: ${warningMessage}\n`);
|
|
576
697
|
return;
|
|
577
698
|
}
|
|
578
|
-
this.ctx
|
|
579
|
-
this.ctx
|
|
699
|
+
addChatChild(this.ctx, new Spacer(1));
|
|
700
|
+
addChatChild(this.ctx, new Text(theme.fg("warning", `${label}: ${warningMessage}`), 1, 0));
|
|
580
701
|
this.ctx.ui.requestRender();
|
|
581
702
|
}
|
|
582
703
|
|
|
583
704
|
showNewVersionNotification(newVersion: string): void {
|
|
584
|
-
this.ctx
|
|
585
|
-
this.ctx
|
|
586
|
-
|
|
705
|
+
addChatChild(this.ctx, new Spacer(1));
|
|
706
|
+
addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
|
|
707
|
+
addChatChild(
|
|
708
|
+
this.ctx,
|
|
587
709
|
new Text(
|
|
588
710
|
theme.bold(theme.fg("warning", "Update Available")) +
|
|
589
711
|
"\n" +
|
|
@@ -593,7 +715,7 @@ export class UiHelpers {
|
|
|
593
715
|
0,
|
|
594
716
|
),
|
|
595
717
|
);
|
|
596
|
-
this.ctx
|
|
718
|
+
addChatChild(this.ctx, new DynamicBorder(text => theme.fg("warning", text)));
|
|
597
719
|
this.ctx.ui.requestRender();
|
|
598
720
|
}
|
|
599
721
|
|
|
@@ -859,12 +981,12 @@ export class UiHelpers {
|
|
|
859
981
|
// removeChild() would tear them down before re-adding.
|
|
860
982
|
for (const component of this.ctx.pendingBashComponents) {
|
|
861
983
|
this.ctx.pendingMessagesContainer.detachChild(component);
|
|
862
|
-
this.ctx
|
|
984
|
+
addChatChild(this.ctx, component);
|
|
863
985
|
}
|
|
864
986
|
this.ctx.pendingBashComponents = [];
|
|
865
987
|
for (const component of this.ctx.pendingPythonComponents) {
|
|
866
988
|
this.ctx.pendingMessagesContainer.detachChild(component);
|
|
867
|
-
this.ctx
|
|
989
|
+
addChatChild(this.ctx, component);
|
|
868
990
|
}
|
|
869
991
|
this.ctx.pendingPythonComponents = [];
|
|
870
992
|
}
|
|
@@ -146,6 +146,7 @@ const TYPING_REFRESH_INTERVAL_MS = 4_000;
|
|
|
146
146
|
const QUEUED_REACTION = "👀";
|
|
147
147
|
const PENDING_TOPIC_FRAME_LIMIT = 20;
|
|
148
148
|
const SEEN_UPDATE_ID_LIMIT = 1_000;
|
|
149
|
+
const ORPHAN_TOPIC_GRACE_MS = 60_000;
|
|
149
150
|
const CONSUMED_REACTION = "✅";
|
|
150
151
|
|
|
151
152
|
function splitTelegramPlainText(text: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
@@ -1414,24 +1415,33 @@ export class TelegramNotificationDaemon {
|
|
|
1414
1415
|
async scanRoots(): Promise<void> {
|
|
1415
1416
|
const paths = daemonPaths(this.opts.settings.getAgentDir());
|
|
1416
1417
|
const rootState = await readJson<{ roots?: string[] }>(this.fsImpl, paths.roots);
|
|
1418
|
+
const endpointSessionIds = new Set<string>();
|
|
1419
|
+
let allRootsReadable = true;
|
|
1417
1420
|
for (const root of rootState?.roots ?? []) {
|
|
1418
1421
|
const dir = path.join(root, "notifications");
|
|
1419
1422
|
let files: string[];
|
|
1420
1423
|
try {
|
|
1421
1424
|
files = await this.fsImpl.readdir(dir);
|
|
1422
1425
|
} catch {
|
|
1426
|
+
allRootsReadable = false;
|
|
1423
1427
|
continue;
|
|
1424
1428
|
}
|
|
1425
1429
|
for (const file of files.filter(item => item.endsWith(".json"))) {
|
|
1426
1430
|
const sessionId = path.basename(file, ".json");
|
|
1431
|
+
endpointSessionIds.add(sessionId);
|
|
1427
1432
|
if (this.sessions.has(sessionId)) continue;
|
|
1428
1433
|
try {
|
|
1429
1434
|
const endpoint = readEndpoint(path.join(dir, file));
|
|
1430
1435
|
// Skip endpoint files whose owning process is gone or that are
|
|
1431
1436
|
// explicitly stale (e.g. a hard-closed session): reconnecting
|
|
1432
|
-
// would chase a dead, token-bearing record forever.
|
|
1437
|
+
// would chase a dead, token-bearing record forever. Once the
|
|
1438
|
+
// associated topic is past the grace window, reap it through the
|
|
1439
|
+
// same best-effort delete path as graceful session shutdown.
|
|
1433
1440
|
const pidAlive = this.opts.pidAlive ?? defaultPidAlive;
|
|
1434
|
-
if (endpoint.stale || (endpoint.pid !== undefined && !pidAlive(endpoint.pid)))
|
|
1441
|
+
if (endpoint.stale || (endpoint.pid !== undefined && !pidAlive(endpoint.pid))) {
|
|
1442
|
+
await this.deleteOrphanedTopic(sessionId);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1435
1445
|
const endpointKey = endpointGenerationKey(endpoint.url, endpoint.token);
|
|
1436
1446
|
if (this.closedEndpointKeys.get(sessionId) === endpointKey) continue;
|
|
1437
1447
|
this.closedEndpointKeys.delete(sessionId);
|
|
@@ -1439,6 +1449,12 @@ export class TelegramNotificationDaemon {
|
|
|
1439
1449
|
} catch {}
|
|
1440
1450
|
}
|
|
1441
1451
|
}
|
|
1452
|
+
if (allRootsReadable) {
|
|
1453
|
+
for (const sessionId of this.topics.sessionIds()) {
|
|
1454
|
+
if (!this.sessions.has(sessionId) && !endpointSessionIds.has(sessionId))
|
|
1455
|
+
await this.deleteOrphanedTopic(sessionId);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1442
1458
|
}
|
|
1443
1459
|
|
|
1444
1460
|
connectSession(sessionId: string, url: string, token: string): void {
|
|
@@ -1683,6 +1699,16 @@ export class TelegramNotificationDaemon {
|
|
|
1683
1699
|
}
|
|
1684
1700
|
}
|
|
1685
1701
|
|
|
1702
|
+
private topicPastOrphanGrace(sessionId: string): boolean {
|
|
1703
|
+
const record = this.topics.get(sessionId);
|
|
1704
|
+
return record !== undefined && this.runtime.now() - record.createdAt >= ORPHAN_TOPIC_GRACE_MS;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private async deleteOrphanedTopic(sessionId: string): Promise<void> {
|
|
1708
|
+
if (!this.topicPastOrphanGrace(sessionId)) return;
|
|
1709
|
+
await this.deleteTopic(sessionId);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1686
1712
|
/** Best-effort delete of a session topic once its local notification endpoint shuts down. */
|
|
1687
1713
|
private async deleteTopic(sessionId: string): Promise<void> {
|
|
1688
1714
|
const record = this.topics.get(sessionId);
|
|
@@ -8,8 +8,8 @@ Invoke another available skill in the current turn.
|
|
|
8
8
|
<instruction>
|
|
9
9
|
- `name` is the skill name as it appears in `/skill:<name>` (e.g. `ralplan`, `ultragoal`, `team`, `deep-interview`)
|
|
10
10
|
- `args` is the free-form argument string the skill would receive after `/skill:<name>` on the command line
|
|
11
|
-
- The tool loads the callee's SKILL.md into the current turn and handles
|
|
12
|
-
- The chain is refused while
|
|
11
|
+
- The tool loads the callee's SKILL.md into the current turn and handles native workflow caller→callee state handoff when the caller is one of the built-in SKC workflows.
|
|
12
|
+
- The chain is refused while a native workflow caller is still active. If your current skill is one of `deep-interview`, `ralplan`, `ultragoal`, or `team` and has not yet reached a terminal phase, prepare it first with `skc state <skill> write --input '{"current_phase":"handoff"}' --json`; no other handoff command is needed. Runtime project/user skills do not use `skc state <skill>`.
|
|
13
13
|
- Call once per chain step. To chain `A → B → C`, A calls `skill(B)`; B's next agent turn calls `skill(C)`.
|
|
14
14
|
</instruction>
|
|
15
15
|
|
|
@@ -238,10 +238,11 @@ export async function listMCPServers(filePath: string): Promise<string[]> {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
|
-
* Set a server's autoload flag
|
|
241
|
+
* Set a server's autoload flag for explicit runtime MCP consumers.
|
|
242
242
|
*
|
|
243
|
-
* Autoload defaults to true, so turning it on
|
|
244
|
-
* config file free of redundant fields; turning it
|
|
243
|
+
* Autoload defaults to true for consumers that honor it, so turning it on
|
|
244
|
+
* removes the key to keep the config file free of redundant fields; turning it
|
|
245
|
+
* off writes `autoload: false`.
|
|
245
246
|
*
|
|
246
247
|
* @throws Error if the server doesn't exist
|
|
247
248
|
*/
|
|
@@ -56,6 +56,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
|
56
56
|
};
|
|
57
57
|
if (server.args) config.args = server.args;
|
|
58
58
|
if (server.env) config.env = server.env;
|
|
59
|
+
if (server.noInheritEnv !== undefined) config.noInheritEnv = server.noInheritEnv;
|
|
59
60
|
if (server.cwd) config.cwd = server.cwd;
|
|
60
61
|
return config;
|
|
61
62
|
}
|
package/src/runtime-mcp/types.ts
CHANGED
|
@@ -62,9 +62,10 @@ interface MCPServerConfigBase {
|
|
|
62
62
|
/** Whether this server is enabled (default: true) */
|
|
63
63
|
enabled?: boolean;
|
|
64
64
|
/**
|
|
65
|
-
* Whether
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Whether an explicit runtime MCP consumer should connect this server
|
|
66
|
+
* automatically when that consumer starts (default: true). Normal standalone
|
|
67
|
+
* `skc`, `skc --tmux`, and print-mode sessions do not consume `skc mcp`
|
|
68
|
+
* registrations today.
|
|
68
69
|
*/
|
|
69
70
|
autoload?: boolean;
|
|
70
71
|
/** Connection timeout in milliseconds (default: 30000) */
|