@oh-my-pi/pi-coding-agent 17.1.3 → 17.1.4
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 +54 -0
- package/dist/cli.js +3759 -3751
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
|
@@ -78,14 +78,35 @@ const SHELL_ESCAPED_PATH_CHAR_REGEX = /\\([\\\s'"()[\]{}&;<>|?*!$`])/g;
|
|
|
78
78
|
const URI_SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:/i;
|
|
79
79
|
const FILE_URI_REGEX = /^file:\/\//i;
|
|
80
80
|
const WINDOWS_DRIVE_PATH_REGEX = /^[a-z]:[\\/]/i;
|
|
81
|
+
/**
|
|
82
|
+
* Alternation of the filesystem prefixes that make a path unambiguously
|
|
83
|
+
* absolute (POSIX root, home, `file://`, UNC, Windows drive). Shared by
|
|
84
|
+
* {@link ABSOLUTE_PATH_PREFIX_REGEX} and {@link INTERIOR_PATH_ANCHOR_REGEX} so
|
|
85
|
+
* the leading-anchor test and the second-anchor test can never disagree about
|
|
86
|
+
* what counts as the start of a path.
|
|
87
|
+
*/
|
|
88
|
+
const ABSOLUTE_PATH_PREFIX_SOURCE = String.raw`(?:\/|~\/|file:\/\/|\\\\|[A-Za-z]:[\\/])`;
|
|
81
89
|
/**
|
|
82
90
|
* Whole-string anchor for paths that are unambiguously absolute. Restricts the
|
|
83
|
-
* "treat the entire
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
91
|
+
* "treat the entire text as one path" pass of {@link extractWholeTextImagePath}
|
|
92
|
+
* to inputs that start with a clearly-anchored filesystem prefix, so prose
|
|
93
|
+
* containing a path-shaped fragment (e.g. "see /tmp/x.png") never hijacks the
|
|
94
|
+
* smart fallback.
|
|
87
95
|
*/
|
|
88
|
-
const ABSOLUTE_PATH_PREFIX_REGEX =
|
|
96
|
+
const ABSOLUTE_PATH_PREFIX_REGEX = new RegExp(`^${ABSOLUTE_PATH_PREFIX_SOURCE}`);
|
|
97
|
+
/**
|
|
98
|
+
* A second path anchor after *unescaped* whitespace — the signature of a
|
|
99
|
+
* multi-path payload (`/tmp/a.png /tmp/b shot.png`, `/tmp/a.png ./b shot.png`)
|
|
100
|
+
* rather than of one path whose name merely contains spaces. Anchors are the
|
|
101
|
+
* absolute prefixes plus dot-relative starts (`./`, `../`, `.\`), which never
|
|
102
|
+
* begin a component of a single sane path. Bare relatives (`dir/b shot.png`)
|
|
103
|
+
* are deliberately NOT anchors: an interior `token/` after a space is exactly
|
|
104
|
+
* the shape of a single path with a spaced directory name
|
|
105
|
+
* (`/Users/me/My Photos/shot 1.png`), which this fallback exists to recover.
|
|
106
|
+
* Escaped whitespace (`/tmp/My\ Photos/x.png`) is exempt: the escape is the
|
|
107
|
+
* terminal asserting the space belongs to the path.
|
|
108
|
+
*/
|
|
109
|
+
const INTERIOR_PATH_ANCHOR_REGEX = new RegExp(String.raw`(?<!\\)\s(?:${ABSOLUTE_PATH_PREFIX_SOURCE}|\.\.?[\\/])`);
|
|
89
110
|
|
|
90
111
|
/** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
|
|
91
112
|
* deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
|
|
@@ -228,16 +249,28 @@ export function extractPastePathsFromText(text: string): string[] | undefined {
|
|
|
228
249
|
return extractExplicitPathSegments(text);
|
|
229
250
|
}
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Whole-text-as-path pass shared by {@link extractImagePastePathsFromText}
|
|
254
|
+
* and {@link extractImagePathFromText}: treat the entire text as one path
|
|
255
|
+
* when it is anchored by {@link ABSOLUTE_PATH_PREFIX_REGEX}, contains no
|
|
256
|
+
* newlines, and points at a supported image extension. Recovers single paths
|
|
257
|
+
* whose unescaped spaces defeat the segment splitter (macOS screenshot names).
|
|
258
|
+
*
|
|
259
|
+
* Refuses payloads carrying a second {@link INTERIOR_PATH_ANCHOR_REGEX} anchor.
|
|
260
|
+
* Dragging two files at once emits `/tmp/a.png /tmp/b shot.png`, which the
|
|
261
|
+
* splitter also refuses (`shot.png` is not explicit); swallowing it as one path
|
|
262
|
+
* attaches nothing, and `handleImagePathPaste`'s ENOENT branch only surfaces a
|
|
263
|
+
* status — unlike its other failure branches it never re-pastes the text — so
|
|
264
|
+
* both paths would vanish. Genuinely ambiguous input lands here too (a
|
|
265
|
+
* directory whose name ends in a space, as in `/tmp/odd dir /sub/x.png`); a
|
|
266
|
+
* plain text paste is the losing-nothing outcome, so ambiguity resolves that way.
|
|
267
|
+
*/
|
|
268
|
+
function extractWholeTextImagePath(text: string): string | undefined {
|
|
269
|
+
const trimmed = text.trim();
|
|
270
|
+
if (!trimmed || /[\r\n]/.test(trimmed) || !ABSOLUTE_PATH_PREFIX_REGEX.test(trimmed)) return undefined;
|
|
271
|
+
if (INTERIOR_PATH_ANCHOR_REGEX.test(trimmed)) return undefined;
|
|
272
|
+
const wholePath = normalizePastedPath(trimmed);
|
|
273
|
+
return wholePath && isExplicitPastedPath(wholePath) && isImagePath(wholePath) ? wholePath : undefined;
|
|
241
274
|
}
|
|
242
275
|
|
|
243
276
|
/**
|
|
@@ -245,10 +278,35 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi
|
|
|
245
278
|
* payload that has already been stripped of the `\x1b[200~` / `\x1b[201~`
|
|
246
279
|
* markers — used by the assembled-paste router in {@link CustomEditor.handleInput}
|
|
247
280
|
* so split bracketed pastes get the same image-path detection as single-chunk ones.
|
|
281
|
+
*
|
|
282
|
+
* When the segment splitter fails (an unescaped space in a real path breaks
|
|
283
|
+
* its every-segment-is-a-path invariant), falls back to
|
|
284
|
+
* {@link extractWholeTextImagePath}, so a dropped macOS screenshot
|
|
285
|
+
* (`Screenshot 2026-06-25 at 1.23.45 PM.png`) attaches as an image instead of
|
|
286
|
+
* degrading to literal text (#6578).
|
|
248
287
|
*/
|
|
249
288
|
export function extractImagePastePathsFromText(text: string): string[] | undefined {
|
|
250
289
|
const paths = extractPastePathsFromText(text);
|
|
251
|
-
return paths
|
|
290
|
+
if (paths !== undefined) return paths.every(isImagePath) ? paths : undefined;
|
|
291
|
+
const wholePath = extractWholeTextImagePath(text);
|
|
292
|
+
return wholePath ? [wholePath] : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function bracketedPastePayload(data: string): string | undefined {
|
|
296
|
+
if (!data.startsWith(BRACKETED_PASTE_START)) return undefined;
|
|
297
|
+
const endIndex = data.indexOf(BRACKETED_PASTE_END, BRACKETED_PASTE_START.length);
|
|
298
|
+
if (endIndex === -1 || endIndex + BRACKETED_PASTE_END.length !== data.length) return undefined;
|
|
299
|
+
return data.slice(BRACKETED_PASTE_START.length, endIndex);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function extractBracketedPastePaths(data: string): string[] | undefined {
|
|
303
|
+
const payload = bracketedPastePayload(data);
|
|
304
|
+
return payload === undefined ? undefined : extractExplicitPathSegments(payload);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function extractBracketedImagePastePaths(data: string): string[] | undefined {
|
|
308
|
+
const payload = bracketedPastePayload(data);
|
|
309
|
+
return payload === undefined ? undefined : extractImagePastePathsFromText(payload);
|
|
252
310
|
}
|
|
253
311
|
|
|
254
312
|
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
@@ -272,25 +330,17 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
|
|
272
330
|
* ambiguous multi-path clipboard text like `/tmp/a.png /tmp/b.png`
|
|
273
331
|
* still falls through to the text fallback instead of being mis-loaded
|
|
274
332
|
* as one giant path).
|
|
275
|
-
* 2.
|
|
276
|
-
* (every segment must look like an explicit path; an unescaped
|
|
277
|
-
* a real path breaks that).
|
|
278
|
-
*
|
|
279
|
-
* fragment ("see /tmp/x.png") never hijacks the smart fallback. This
|
|
280
|
-
* is what recovers macOS screenshot filenames like
|
|
333
|
+
* 2. {@link extractWholeTextImagePath} — only reached when the splitter
|
|
334
|
+
* failed (every segment must look like an explicit path; an unescaped
|
|
335
|
+
* space in a real path breaks that). This is what recovers macOS
|
|
336
|
+
* screenshot filenames like
|
|
281
337
|
* `/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png`.
|
|
282
338
|
*/
|
|
283
339
|
export function extractImagePathFromText(text: string): string | undefined {
|
|
284
340
|
const paths = extractPastePathsFromText(text);
|
|
285
341
|
if (paths?.length === 1 && isImagePath(paths[0])) return paths[0];
|
|
286
342
|
if (paths !== undefined) return undefined;
|
|
287
|
-
|
|
288
|
-
if (!trimmed || /[\r\n]/.test(trimmed) || !ABSOLUTE_PATH_PREFIX_REGEX.test(trimmed)) return undefined;
|
|
289
|
-
const wholePath = normalizePastedPath(trimmed);
|
|
290
|
-
if (wholePath && isExplicitPastedPath(wholePath) && isImagePath(wholePath)) {
|
|
291
|
-
return wholePath;
|
|
292
|
-
}
|
|
293
|
-
return undefined;
|
|
343
|
+
return extractWholeTextImagePath(text);
|
|
294
344
|
}
|
|
295
345
|
|
|
296
346
|
/**
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getPathsForTab,
|
|
19
19
|
getType,
|
|
20
20
|
getUi,
|
|
21
|
+
isCredential,
|
|
21
22
|
SETTING_TABS,
|
|
22
23
|
type SettingPath,
|
|
23
24
|
type SettingTab,
|
|
@@ -192,7 +193,10 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
|
|
|
192
193
|
if (options) {
|
|
193
194
|
return { ...base, type: "submenu", options };
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
+
// One classification drives both surfaces: a setting marked `credential`
|
|
197
|
+
// masks here too, so the panel cannot display one that only the CLI knows
|
|
198
|
+
// to redact.
|
|
199
|
+
return { ...base, type: "text", secret: isCredential(path) };
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
if (schemaType === "array") {
|
|
@@ -2,7 +2,7 @@ import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
3
3
|
import { getStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
4
4
|
import { type Component, Loader, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { logger, prompt, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
7
7
|
import { extractTextContent } from "../../commit/utils";
|
|
8
8
|
import { settings } from "../../config/settings";
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
readArgsHaveTarget,
|
|
16
16
|
} from "../../modes/components/read-tool-group";
|
|
17
17
|
import { TodoReminderComponent } from "../../modes/components/todo-reminder";
|
|
18
|
-
import { ToolExecutionComponent } from "../../modes/components/tool-execution";
|
|
18
|
+
import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/components/tool-execution";
|
|
19
19
|
import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
|
|
20
20
|
import { createUsageRowBlock } from "../../modes/components/usage-row";
|
|
21
21
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
@@ -89,6 +89,17 @@ export class EventController {
|
|
|
89
89
|
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
|
90
90
|
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
91
91
|
#toolTimelineComponents = new Map<string, Component>();
|
|
92
|
+
// Completions that arrived before any component existed for their call id.
|
|
93
|
+
// Cursor's server-resolved tools (todo) emit `tool_execution_end` through a
|
|
94
|
+
// synchronous callback fired mid-parse, while the `toolcall_start` for the
|
|
95
|
+
// same call rides `AssistantMessageEventStream` and is delivered a microtask
|
|
96
|
+
// later. When the server packs start and completion into one HTTP/2 chunk
|
|
97
|
+
// the completion is handled FIRST — with no `pendingTools` entry to settle.
|
|
98
|
+
// Dropping it would strand the card the streamed block creates moments
|
|
99
|
+
// later, so the event is held here and replayed the moment that component
|
|
100
|
+
// materializes (`#handleMessageUpdate`). Keyed by call id; ids are unique
|
|
101
|
+
// per turn, and the map is cleared with the other transcript anchors.
|
|
102
|
+
#orphanedToolCompletions = new Map<string, Extract<AgentSessionEvent, { type: "tool_execution_end" }>>();
|
|
92
103
|
#postToolAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
93
104
|
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
|
94
105
|
// Assistant component whose turn-ending error is currently mirrored in the
|
|
@@ -334,6 +345,7 @@ export class EventController {
|
|
|
334
345
|
this.#renderedCustomMessages.clear();
|
|
335
346
|
this.#lastIntent = undefined;
|
|
336
347
|
this.#toolTimelineComponents.clear();
|
|
348
|
+
this.#orphanedToolCompletions.clear();
|
|
337
349
|
this.#postToolAssistantComponents.clear();
|
|
338
350
|
this.#backgroundTaskCallIds.clear();
|
|
339
351
|
this.#approvalAttentionToolCallIds.clear();
|
|
@@ -422,6 +434,7 @@ export class EventController {
|
|
|
422
434
|
|
|
423
435
|
async #handleAgentStart(_event: Extract<AgentSessionEvent, { type: "agent_start" }>): Promise<void> {
|
|
424
436
|
this.#toolTimelineComponents.clear();
|
|
437
|
+
this.#orphanedToolCompletions.clear();
|
|
425
438
|
this.#postToolAssistantComponents.clear();
|
|
426
439
|
this.#lastIntent = undefined;
|
|
427
440
|
this.#readToolCallArgs.clear();
|
|
@@ -777,7 +790,14 @@ export class EventController {
|
|
|
777
790
|
this.#toolArgsReveal.finish(content.id);
|
|
778
791
|
renderArgs = content.arguments;
|
|
779
792
|
}
|
|
780
|
-
|
|
793
|
+
// `message_update` is cumulative — every update re-lists all blocks
|
|
794
|
+
// of the streaming message — so creation must also be guarded by the
|
|
795
|
+
// timeline map: `pendingTools` loses the id the moment a completion
|
|
796
|
+
// settles the card, and for server-resolved (Cursor) tools that can
|
|
797
|
+
// happen while the message is still streaming. Without the second
|
|
798
|
+
// check the next cumulative update would recreate a card for a call
|
|
799
|
+
// that already finished, permanently pending.
|
|
800
|
+
if (!this.ctx.pendingTools.has(content.id) && !this.#toolTimelineComponents.has(content.id)) {
|
|
781
801
|
this.#resolveDisplaceablePoll(content.name);
|
|
782
802
|
this.#resetReadGroup();
|
|
783
803
|
const component = new ToolExecutionComponent(
|
|
@@ -799,6 +819,16 @@ export class EventController {
|
|
|
799
819
|
this.ctx.pendingTools.set(content.id, component);
|
|
800
820
|
this.#toolTimelineComponents.set(content.id, component);
|
|
801
821
|
this.#toolArgsReveal.bind(content.id, component);
|
|
822
|
+
// A held completion for this call means its `tool_execution_end`
|
|
823
|
+
// outran this streamed block (see #orphanedToolCompletions).
|
|
824
|
+
// Attach it now that the card exists so it settles immediately
|
|
825
|
+
// instead of animating forever. Only the component is settled —
|
|
826
|
+
// the handler's other side effects already ran on first arrival.
|
|
827
|
+
const orphan = this.#orphanedToolCompletions.get(content.id);
|
|
828
|
+
if (orphan) {
|
|
829
|
+
this.#orphanedToolCompletions.delete(content.id);
|
|
830
|
+
this.#settleHeldCompletion(component, orphan);
|
|
831
|
+
}
|
|
802
832
|
} else {
|
|
803
833
|
const component = this.ctx.pendingTools.get(content.id);
|
|
804
834
|
if (component) {
|
|
@@ -1066,6 +1096,44 @@ export class EventController {
|
|
|
1066
1096
|
}
|
|
1067
1097
|
}
|
|
1068
1098
|
|
|
1099
|
+
/**
|
|
1100
|
+
* Attach a held completion to the component that was created for it after
|
|
1101
|
+
* the fact (see {@link #orphanedToolCompletions}) and settle the card.
|
|
1102
|
+
*
|
|
1103
|
+
* Deliberately NOT a re-entry into `#handleToolExecutionEnd`: every
|
|
1104
|
+
* user-facing side effect of that handler — the todo panel refresh, the
|
|
1105
|
+
* failure warning, plan approval, the terminal-title transition — already
|
|
1106
|
+
* ran when the completion first arrived. Replaying the whole handler would
|
|
1107
|
+
* show the same warning twice and re-push an identical `setTodos`. Only the
|
|
1108
|
+
* component half was missed, because the component did not exist yet.
|
|
1109
|
+
*/
|
|
1110
|
+
#settleHeldCompletion(
|
|
1111
|
+
component: ToolExecutionHandle,
|
|
1112
|
+
event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
|
|
1113
|
+
): void {
|
|
1114
|
+
component.updateResult({ ...event.result, isError: event.isError }, false, event.toolCallId);
|
|
1115
|
+
this.ctx.pendingTools.delete(event.toolCallId);
|
|
1116
|
+
if (
|
|
1117
|
+
component instanceof ToolExecutionComponent &&
|
|
1118
|
+
component.isDisplaceableBlock() &&
|
|
1119
|
+
event.toolName === "todo" &&
|
|
1120
|
+
component.canBeDisplacedBy("todo")
|
|
1121
|
+
) {
|
|
1122
|
+
// Mirrors the displacement bookkeeping in `#handleToolExecutionEnd`:
|
|
1123
|
+
// a successful snapshot supersedes the previous live panel.
|
|
1124
|
+
const previous = this.#displaceableTodoComponent;
|
|
1125
|
+
if (previous && previous !== component && previous.isDisplaceableBlock()) {
|
|
1126
|
+
this.#displaceableTodoComponent = undefined;
|
|
1127
|
+
if (this.ctx.chatContainer.isBlockUncommitted(previous)) {
|
|
1128
|
+
this.ctx.chatContainer.removeChild(previous);
|
|
1129
|
+
}
|
|
1130
|
+
previous.seal();
|
|
1131
|
+
}
|
|
1132
|
+
this.#displaceableTodoComponent = component;
|
|
1133
|
+
}
|
|
1134
|
+
this.ctx.ui.requestRender();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1069
1137
|
async #handleToolExecutionEnd(event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>): Promise<void> {
|
|
1070
1138
|
// A transient overlay (auto-compaction / auto-retry / handoff) that ran
|
|
1071
1139
|
// between this tool's start and end could have detached the working
|
|
@@ -1142,6 +1210,15 @@ export class EventController {
|
|
|
1142
1210
|
}
|
|
1143
1211
|
}
|
|
1144
1212
|
this.ctx.ui.requestRender();
|
|
1213
|
+
} else if (event.toolName === "todo") {
|
|
1214
|
+
// No component yet: the streamed block that creates the card has not
|
|
1215
|
+
// been delivered (see #orphanedToolCompletions). Hold the completion
|
|
1216
|
+
// for replay instead of dropping it — scoped to `todo`, the only
|
|
1217
|
+
// tool whose completion is emitted synchronously mid-parse. The
|
|
1218
|
+
// panel/warning side effects below still run NOW, on first arrival;
|
|
1219
|
+
// the replay settles only the component
|
|
1220
|
+
// (`#settleHeldCompletion`), so neither is repeated.
|
|
1221
|
+
this.#orphanedToolCompletions.set(event.toolCallId, event);
|
|
1145
1222
|
}
|
|
1146
1223
|
}
|
|
1147
1224
|
// Update todo display when todo tool completes
|
|
@@ -1154,8 +1231,22 @@ export class EventController {
|
|
|
1154
1231
|
const textContent = event.result.content.find(
|
|
1155
1232
|
(content: { type: string; text?: string }) => content.type === "text",
|
|
1156
1233
|
)?.text;
|
|
1234
|
+
// This text can be a provider error copied verbatim off the wire (the
|
|
1235
|
+
// Cursor todo bridge forwards the server's string), so it may carry
|
|
1236
|
+
// ANSI escapes, other C0/C1 controls, tabs, newlines, or a line far
|
|
1237
|
+
// wider than the terminal. `showWarning` renders through a plain
|
|
1238
|
+
// `Text`, which strips none of that — an escape reaches the terminal
|
|
1239
|
+
// and can repaint outside the row. `sanitizeText` drops the control
|
|
1240
|
+
// sequences (and returns the same reference when there are none),
|
|
1241
|
+
// then `previewLine` collapses the remaining whitespace and bounds
|
|
1242
|
+
// the width. Sanitizing first matters: truncating before stripping
|
|
1243
|
+
// can cut an escape mid-sequence and leave a dangling introducer.
|
|
1244
|
+
//
|
|
1245
|
+
// This is the render boundary, not the persisted result: the stored
|
|
1246
|
+
// error stays full-fidelity for the transcript and for replays.
|
|
1247
|
+
const detail = textContent ? previewLine(sanitizeText(textContent), TRUNCATE_LENGTHS.LINE) : "";
|
|
1157
1248
|
this.ctx.showWarning(
|
|
1158
|
-
`Todo update failed${
|
|
1249
|
+
`Todo update failed${detail ? `: ${detail}` : ". Progress may be stale until todo succeeds."}`,
|
|
1159
1250
|
);
|
|
1160
1251
|
}
|
|
1161
1252
|
// Plan approval rides a `write` to xd://propose: the dispatch metadata on
|
|
@@ -1236,6 +1327,7 @@ export class EventController {
|
|
|
1236
1327
|
this.#readToolCallArgs.clear();
|
|
1237
1328
|
this.#readToolCallAssistantComponents.clear();
|
|
1238
1329
|
this.#toolTimelineComponents.clear();
|
|
1330
|
+
this.#orphanedToolCompletions.clear();
|
|
1239
1331
|
this.#postToolAssistantComponents.clear();
|
|
1240
1332
|
this.#resetReadGroup();
|
|
1241
1333
|
// The turn is over: nothing else lands this turn, so the waiting poll is
|
|
@@ -420,6 +420,11 @@ export class SelectorController {
|
|
|
420
420
|
this.ctx.session.setAutoCompactionEnabled(value as boolean);
|
|
421
421
|
this.ctx.statusLine.setAutoCompactEnabled(value as boolean);
|
|
422
422
|
break;
|
|
423
|
+
case "advisor.enabled":
|
|
424
|
+
this.ctx.session.setAdvisorEnabled(value as boolean);
|
|
425
|
+
this.ctx.statusLine.invalidate();
|
|
426
|
+
this.ctx.ui.requestRender();
|
|
427
|
+
break;
|
|
423
428
|
case "steeringMode":
|
|
424
429
|
this.ctx.session.setSteeringMode(value as "all" | "one-at-a-time");
|
|
425
430
|
break;
|
|
@@ -1270,6 +1275,15 @@ export class SelectorController {
|
|
|
1270
1275
|
this.ctx.editor.setText(result.editorText);
|
|
1271
1276
|
}
|
|
1272
1277
|
this.ctx.showStatus("Navigated to selected point");
|
|
1278
|
+
|
|
1279
|
+
// Re-answering a past `ask` commits a new sibling answer but,
|
|
1280
|
+
// unlike a live `ask`, leaves the agent idle. Resume it now —
|
|
1281
|
+
// after the transcript rebuild above — so the model consumes the
|
|
1282
|
+
// new answer without the resumed turn rendering against the stale
|
|
1283
|
+
// pre-rebuild UI (issue #6483).
|
|
1284
|
+
if (result.askReanswerCommitted) {
|
|
1285
|
+
this.ctx.session.resumeAfterAskReanswer();
|
|
1286
|
+
}
|
|
1273
1287
|
} catch (error) {
|
|
1274
1288
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1275
1289
|
} finally {
|
|
@@ -1395,6 +1395,11 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1395
1395
|
return;
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
|
+
if (action === "reset" && this.vibeModeEnabled) {
|
|
1399
|
+
this.disableLoopMode("Exit vibe mode before using reset loops. Loop mode disabled.");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1398
1403
|
if (!consumeLoopLimitIteration(this.loopLimit)) {
|
|
1399
1404
|
this.disableLoopMode("Loop limit reached. Loop mode disabled.");
|
|
1400
1405
|
return;
|
|
@@ -3708,6 +3713,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3708
3713
|
return;
|
|
3709
3714
|
}
|
|
3710
3715
|
|
|
3716
|
+
// resolveApprovedPlan may return a newer draft than the path recorded in
|
|
3717
|
+
// plan-mode state. `AgentSession.#buildPlanModeMessage()` reads that state,
|
|
3718
|
+
// so if the operator refines (or dismisses and keeps planning) the next
|
|
3719
|
+
// planning turn must target the plan just reviewed — promote the reviewed
|
|
3720
|
+
// path into plan-mode state now, mirroring the print-mode approval handler.
|
|
3721
|
+
const planState = this.session.getPlanModeState();
|
|
3722
|
+
if (planState?.enabled && planState.planFilePath !== planFilePath) {
|
|
3723
|
+
this.session.setPlanModeState({ ...planState, planFilePath });
|
|
3724
|
+
this.sessionManager.appendModeChange("plan", { planFilePath });
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3711
3727
|
const contextUsage = this.#getPlanApprovalContextUsage();
|
|
3712
3728
|
const keepContextLabel = this.#formatKeepContextLabel(contextUsage);
|
|
3713
3729
|
const keepContextDisabled = this.#isKeepContextDisabled(contextUsage);
|
|
@@ -4432,6 +4448,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4432
4448
|
this.#commandController.handleContextCommand();
|
|
4433
4449
|
}
|
|
4434
4450
|
|
|
4451
|
+
#vibeSessionTransitionBlocked(): boolean {
|
|
4452
|
+
if (!this.vibeModeEnabled) return false;
|
|
4453
|
+
this.showWarning("Exit vibe mode first.");
|
|
4454
|
+
return true;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4435
4457
|
#prepareSessionSwitch(): void {
|
|
4436
4458
|
this.#btwController.dispose();
|
|
4437
4459
|
this.#omfgController.dispose();
|
|
@@ -4440,28 +4462,32 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4440
4462
|
this.#hidePlanReview();
|
|
4441
4463
|
}
|
|
4442
4464
|
|
|
4443
|
-
handleClearCommand(): Promise<void> {
|
|
4465
|
+
async handleClearCommand(): Promise<void> {
|
|
4466
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4444
4467
|
this.#prepareSessionSwitch();
|
|
4445
|
-
|
|
4468
|
+
await this.#commandController.handleClearCommand();
|
|
4446
4469
|
}
|
|
4447
4470
|
|
|
4448
4471
|
handleFreshCommand(): Promise<void> {
|
|
4449
4472
|
return this.#commandController.handleFreshCommand();
|
|
4450
4473
|
}
|
|
4451
4474
|
|
|
4452
|
-
handleDropCommand(): Promise<void> {
|
|
4475
|
+
async handleDropCommand(): Promise<void> {
|
|
4476
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4453
4477
|
this.#prepareSessionSwitch();
|
|
4454
|
-
|
|
4478
|
+
await this.#commandController.handleDropCommand();
|
|
4455
4479
|
}
|
|
4456
4480
|
|
|
4457
|
-
handleForkCommand(): Promise<void> {
|
|
4481
|
+
async handleForkCommand(): Promise<void> {
|
|
4482
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4458
4483
|
this.#btwController.dispose();
|
|
4459
4484
|
this.#omfgController.dispose();
|
|
4460
|
-
|
|
4485
|
+
await this.#commandController.handleForkCommand();
|
|
4461
4486
|
}
|
|
4462
4487
|
|
|
4463
|
-
handleMoveCommand(targetPath?: string): Promise<void> {
|
|
4464
|
-
|
|
4488
|
+
async handleMoveCommand(targetPath?: string): Promise<void> {
|
|
4489
|
+
if (this.#vibeSessionTransitionBlocked()) return;
|
|
4490
|
+
await this.#commandController.handleMoveCommand(targetPath);
|
|
4465
4491
|
}
|
|
4466
4492
|
|
|
4467
4493
|
handleRenameCommand(title: string): Promise<void> {
|
|
@@ -56,6 +56,22 @@ const EMPTY_STRING_PARTS: string[] = [];
|
|
|
56
56
|
const EMPTY_TOOLS: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">> = [];
|
|
57
57
|
const EMPTY_SKILLS: readonly Skill[] = [];
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Skills actually rendered into the system prompt, mirroring the filter in
|
|
61
|
+
* `buildSystemPrompt` (`system-prompt.ts`): the `read` tool must be present so
|
|
62
|
+
* the model can fetch skill content, and skills with frontmatter `hide: true`
|
|
63
|
+
* (or `disable-model-invocation`, normalized onto `hide`) are excluded.
|
|
64
|
+
* Accounting must count only these so the Skills category and the System-prompt
|
|
65
|
+
* subtraction stay aligned with the provider-facing prompt.
|
|
66
|
+
*/
|
|
67
|
+
function renderedSkills(
|
|
68
|
+
skills: readonly Skill[],
|
|
69
|
+
tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
|
|
70
|
+
): readonly Skill[] {
|
|
71
|
+
if (!tools.some(tool => tool.name === "read")) return EMPTY_SKILLS;
|
|
72
|
+
return skills.filter(skill => skill.hide !== true);
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
export function estimateSkillsTokens(skills: readonly Skill[]): number {
|
|
60
76
|
const fragments: string[] = [];
|
|
61
77
|
for (const skill of skills) {
|
|
@@ -171,8 +187,9 @@ export function computeNonMessageBreakdown(session: NonMessageTokenSource): {
|
|
|
171
187
|
} {
|
|
172
188
|
const entry = nonMessageTokenCacheEntry(session);
|
|
173
189
|
if (entry.breakdown) return entry.breakdown;
|
|
174
|
-
const
|
|
175
|
-
const
|
|
190
|
+
const tools = session.agent?.state?.tools ?? EMPTY_TOOLS;
|
|
191
|
+
const skillsTokens = estimateSkillsTokens(renderedSkills(session.skills ?? EMPTY_SKILLS, tools));
|
|
192
|
+
const toolsTokens = estimateToolSchemaTokens(tools);
|
|
176
193
|
const systemPromptParts = session.systemPrompt ?? EMPTY_STRING_PARTS;
|
|
177
194
|
const systemContextTokens = countTokens(systemPromptParts.slice(1));
|
|
178
195
|
const systemPromptTokens = Math.max(0, countTokens(systemPromptParts[0] ?? "") - skillsTokens);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
1
2
|
import { ToolError } from "../tools/tool-errors";
|
|
2
3
|
|
|
3
4
|
/** Shape forwarded from the plan-proposal handler to InteractiveMode's
|
|
@@ -149,8 +150,9 @@ export interface ResolvedApprovedPlan {
|
|
|
149
150
|
|
|
150
151
|
/** Locate the plan file the agent wrote and finalize its title — without
|
|
151
152
|
* renaming anything. Tries, in order: the slug derived from `extra.title`
|
|
152
|
-
* (`local://<slug>-plan.md`),
|
|
153
|
-
*
|
|
153
|
+
* (`local://<slug>-plan.md`), a state plan that the artifact scan can't see,
|
|
154
|
+
* scanned plan files newest-to-oldest, then the state plan path as a final
|
|
155
|
+
* fallback. Throws a `ToolError` guiding the agent when none exist. */
|
|
154
156
|
export async function resolveApprovedPlan(input: ResolveApprovedPlanInput): Promise<ResolvedApprovedPlan> {
|
|
155
157
|
const ordered: string[] = [];
|
|
156
158
|
const consider = (url: string | undefined): void => {
|
|
@@ -159,6 +161,20 @@ export async function resolveApprovedPlan(input: ResolveApprovedPlanInput): Prom
|
|
|
159
161
|
|
|
160
162
|
const slug = planSlugFromSupplied(input.suppliedTitle);
|
|
161
163
|
consider(slug ? planFileUrlForSlug(slug) : undefined);
|
|
164
|
+
|
|
165
|
+
const listed = input.listPlanFiles ? await input.listPlanFiles() : [];
|
|
166
|
+
// A state plan the scan cannot surface (cwd-relative, or a local file whose
|
|
167
|
+
// name does not end in `plan.md`) has no mtime in `listed` to compete on, so
|
|
168
|
+
// it keeps precedence over scanned artifacts — otherwise a stale older draft
|
|
169
|
+
// could shadow the deliberately-set current plan. A state plan already inside
|
|
170
|
+
// the scan competes purely on the newest-first ordering below (issue #6569).
|
|
171
|
+
// Compare canonical `local://` spellings so a resumed `local:/…` state path
|
|
172
|
+
// still matches the scanner's `local://…` entry (normalizeLocalScheme).
|
|
173
|
+
const canonicalListed = new Set(listed.map(normalizeLocalScheme));
|
|
174
|
+
if (input.statePlanFilePath && !canonicalListed.has(normalizeLocalScheme(input.statePlanFilePath))) {
|
|
175
|
+
consider(input.statePlanFilePath);
|
|
176
|
+
}
|
|
177
|
+
for (const url of listed) consider(url);
|
|
162
178
|
consider(input.statePlanFilePath);
|
|
163
179
|
|
|
164
180
|
for (const url of ordered) {
|
|
@@ -166,14 +182,6 @@ export async function resolveApprovedPlan(input: ResolveApprovedPlanInput): Prom
|
|
|
166
182
|
if (content !== null) return finalizeApprovedPlan(url, content, input.suppliedTitle);
|
|
167
183
|
}
|
|
168
184
|
|
|
169
|
-
if (input.listPlanFiles) {
|
|
170
|
-
for (const url of await input.listPlanFiles()) {
|
|
171
|
-
if (ordered.includes(url)) continue;
|
|
172
|
-
const content = await input.readPlan(url);
|
|
173
|
-
if (content !== null) return finalizeApprovedPlan(url, content, input.suppliedTitle);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
185
|
const target = ordered[0] ?? input.statePlanFilePath;
|
|
178
186
|
throw new ToolError(
|
|
179
187
|
`Plan file not found at ${target}. Write the finalized plan to ${target} before requesting approval.`,
|
|
@@ -59,6 +59,6 @@ Rules are local constraints. You MUST read `rule://<name>` when working in that
|
|
|
59
59
|
{{/if}}
|
|
60
60
|
{{#if secretsEnabled}}
|
|
61
61
|
<redacted-content>
|
|
62
|
-
Some values in tool output are redacted for security. They appear as placeholder tokens such as
|
|
62
|
+
Some values in tool output are redacted for security. They appear as placeholder tokens such as `$$HASH$$`, `$$HASH:CASE$$`, or `$$NAME_HASH:CASE$$` (uppercase-alphanumeric digest, optional case hint, optional friendly-name prefix). These are **not errors** — they are intentional placeholders for sensitive values (API keys, passwords, tokens). Treat them as opaque strings. NEVER attempt to decode, fix, or report them as problems.
|
|
63
63
|
</redacted-content>
|
|
64
64
|
{{/if}}
|
|
@@ -79,6 +79,15 @@ Special URLs for internal resources; with most FS/bash tools they auto-resolve t
|
|
|
79
79
|
{{/if}}
|
|
80
80
|
{{/if}}
|
|
81
81
|
|
|
82
|
+
{{#has tools "computer"}}
|
|
83
|
+
# Computer Use
|
|
84
|
+
The `{{toolRefs.computer}}` tool is explicitly enabled and available in this session.
|
|
85
|
+
- MUST use `{{toolRefs.computer}}` for requests to view or control host desktop applications.
|
|
86
|
+
- NEVER claim Computer Use is unavailable while `{{toolRefs.computer}}` appears in the tool inventory.
|
|
87
|
+
- While fulfilling host-desktop requests, NEVER substitute Browser, Bash, Eval, AppleScript, accessibility commands, or `screencapture` unless the user explicitly requests that mechanism or `{{toolRefs.computer}}` returns an error.
|
|
88
|
+
- Inspect the fresh screenshot returned by every successful `{{toolRefs.computer}}` call before choosing the next action.
|
|
89
|
+
{{/has}}
|
|
90
|
+
|
|
82
91
|
{{#if xdevTools.length}}
|
|
83
92
|
# xd:// Tool Devices
|
|
84
93
|
Additional tools are mounted as virtual devices, executed by writing a JSON args object as `content` to `xd://<tool>` via `{{toolRefs.write}}`.
|
|
@@ -101,7 +110,7 @@ Use tools whenever they improve correctness, completeness, or grounding.
|
|
|
101
110
|
# Tool I/O
|
|
102
111
|
- Prefer relative paths for `path`-like fields.
|
|
103
112
|
{{#if intentTracing}}- Most tools take `{{intentField}}`: a concise intent, present participle, 2–6 words, no period, capitalized.{{/if}}
|
|
104
|
-
{{#if secretsEnabled}}- Redacted
|
|
113
|
+
{{#if secretsEnabled}}- Redacted `$$HASH$$`, `$$HASH:CASE$$`, or `$$NAME_HASH:CASE$$` tokens in output are opaque strings.{{/if}}
|
|
105
114
|
{{#has tools "inspect_image"}}- Image tasks: prefer `{{toolRefs.inspect_image}}` over `{{toolRefs.read}}` to spare session context.{{/has}}
|
|
106
115
|
|
|
107
116
|
# Specialized Tools
|
package/src/sdk.ts
CHANGED
|
@@ -201,6 +201,7 @@ import { getImageGenTools } from "./tools/image-gen";
|
|
|
201
201
|
import { wrapToolWithMetaNotice } from "./tools/output-meta";
|
|
202
202
|
import { isAutoQaEnabled } from "./tools/report-tool-issue";
|
|
203
203
|
import { queueResolveHandler } from "./tools/resolve";
|
|
204
|
+
import { USER_TODO_EDIT_CUSTOM_TYPE } from "./tools/todo";
|
|
204
205
|
import { ttsTool } from "./tools/tts";
|
|
205
206
|
import { resolveActiveRepoContext } from "./utils/active-repo-context";
|
|
206
207
|
import { EventBus } from "./utils/event-bus";
|
|
@@ -2609,6 +2610,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2609
2610
|
getTool: resolveDeviceTool,
|
|
2610
2611
|
getToolContext: () => toolContextStore.getContext(),
|
|
2611
2612
|
emitEvent: event => cursorEventEmitter?.(event),
|
|
2613
|
+
getTodoPhases: () => session.getTodoPhases(),
|
|
2614
|
+
setTodoPhases: phases => session.setTodoPhases(phases),
|
|
2615
|
+
persistTodoPhases: phases => sessionManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases }),
|
|
2612
2616
|
});
|
|
2613
2617
|
|
|
2614
2618
|
// Resolve the inline-descriptors setting against the session-start model.
|