@rmrdeveloper/sideroom-pi 9.0.1 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -40
- package/assets/visual-context/browser/background.js +2 -2
- package/assets/visual-context/browser/popup.js +1 -2
- package/extensions/ask/index.ts +0 -1
- package/extensions/ask/model.ts +1 -1
- package/extensions/ask/selection.ts +0 -4
- package/extensions/done/guard.ts +18 -35
- package/extensions/done/index.ts +0 -1
- package/extensions/done/model.ts +2 -31
- package/extensions/explain/index.ts +4 -7
- package/extensions/explain/prompt.ts +13 -0
- package/extensions/guidelines/execute.ts +16 -2
- package/extensions/guidelines/guard.ts +1 -2
- package/extensions/guidelines/index.ts +7 -7
- package/extensions/guidelines/prompt.ts +0 -2
- package/extensions/guidelines/review.ts +9 -10
- package/extensions/jev/client.ts +0 -7
- package/extensions/jev/context.ts +0 -2
- package/extensions/jev/guard.ts +78 -44
- package/extensions/jev/index.ts +0 -3
- package/extensions/jev/key.ts +0 -3
- package/extensions/jev/model.ts +0 -8
- package/extensions/jev/queue.ts +125 -0
- package/extensions/jev/ui.ts +0 -14
- package/extensions/modified-files/index.ts +0 -1
- package/extensions/monorepo-skills/index.ts +0 -1
- package/extensions/persona/checks.ts +0 -4
- package/extensions/persona/guard.ts +45 -6
- package/extensions/persona/index.ts +4 -2
- package/extensions/persona/model.ts +0 -2
- package/extensions/persona/prompt.ts +0 -2
- package/extensions/rules/checks.ts +0 -5
- package/extensions/rules/index.ts +0 -1
- package/extensions/rules/lexer.ts +1 -1
- package/extensions/shared/check-status.ts +35 -0
- package/extensions/shared/file-path.ts +0 -3
- package/extensions/shared/read-file.ts +0 -2
- package/extensions/shared/settle.ts +78 -13
- package/extensions/todo/guards.ts +40 -84
- package/extensions/todo/index.ts +1 -2
- package/extensions/todo/model.ts +6 -48
- package/extensions/todo/session.ts +1 -4
- package/extensions/visual-context/broker.ts +10 -10
- package/extensions/visual-context/configure.ts +4 -4
- package/extensions/visual-context/files.ts +0 -3
- package/extensions/visual-context/index.ts +8 -9
- package/extensions/visual-context/literal.ts +0 -1
- package/extensions/visual-context/markup.ts +3 -5
- package/extensions/visual-context/model.ts +2 -2
- package/extensions/visual-context/pairing.ts +1 -2
- package/extensions/visual-context/parse-astro.ts +9 -12
- package/extensions/visual-context/parse-jsx.ts +2 -2
- package/extensions/visual-context/parse-vue.ts +1 -1
- package/extensions/visual-context/setup.ts +8 -8
- package/extensions/visual-context/source.ts +0 -3
- package/package.json +15 -9
- package/skills/sideroom-guidelines/SKILL.md +1 -1
- package/skills/sideroom-persona/SKILL.md +9 -7
- package/skills/sideroom-user-stories/SKILL.md +78 -0
- package/skills/sideroom-user-stories/references/review.md +84 -0
- package/skills/sideroom-user-stories/references/templates.md +131 -0
- package/extensions/explain/guard.ts +0 -70
- package/extensions/explain/model.ts +0 -71
|
@@ -39,9 +39,6 @@ const DEBUG_PATTERNS: readonly RegExp[] = [
|
|
|
39
39
|
/\bdbg!\s*\(/,
|
|
40
40
|
];
|
|
41
41
|
|
|
42
|
-
// Keep these patterns on syntax that can only introduce a type. Value-level
|
|
43
|
-
// commas, assignments, and operators may legally surround an identifier named
|
|
44
|
-
// `any`, so they are deliberately absent.
|
|
45
42
|
const EXPLICIT_ANY_PATTERNS: readonly RegExp[] = [
|
|
46
43
|
/\bas\s+any\b/,
|
|
47
44
|
/\b(?:keyof|satisfies)\s+any\b/,
|
|
@@ -54,8 +51,6 @@ const EXPLICIT_ANY_PATTERNS: readonly RegExp[] = [
|
|
|
54
51
|
/^\s*(?:(?:public|private|protected|readonly|static|declare|abstract)\s+)*[$\w]+[?!]?\s*:\s*(?:(?:=>)|[^;,=\n])*\bany\b[^;=\n]*(?:;|=)/,
|
|
55
52
|
];
|
|
56
53
|
|
|
57
|
-
// A suppression is a comment whose first token is the directive; prose that
|
|
58
|
-
// mentions the directive, or a string that quotes it, is not one.
|
|
59
54
|
const SUPPRESSED_TYPE_ERRORS =
|
|
60
55
|
/(?:^|\s)(?:\/\/|\/\*)\s*@ts-(?:ignore|nocheck)\b/;
|
|
61
56
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent';
|
|
5
|
+
|
|
6
|
+
const CHECK_STATUS_EVENT = 'sideroom:check-status';
|
|
7
|
+
|
|
8
|
+
export interface CheckStatus {
|
|
9
|
+
readonly command: string;
|
|
10
|
+
readonly green: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CheckStatusRequest {
|
|
14
|
+
readonly ctx: ExtensionContext;
|
|
15
|
+
status: CheckStatus | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerCheckStatus(
|
|
19
|
+
pi: ExtensionAPI,
|
|
20
|
+
resolve: (ctx: ExtensionContext) => CheckStatus | undefined,
|
|
21
|
+
): void {
|
|
22
|
+
pi.events.on(CHECK_STATUS_EVENT, (payload) => {
|
|
23
|
+
const request = payload as CheckStatusRequest;
|
|
24
|
+
request.status = resolve(request.ctx);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readCheckStatus(
|
|
29
|
+
pi: ExtensionAPI,
|
|
30
|
+
ctx: ExtensionContext,
|
|
31
|
+
): CheckStatus | undefined {
|
|
32
|
+
const request: CheckStatusRequest = { ctx, status: undefined };
|
|
33
|
+
pi.events.emit(CHECK_STATUS_EVENT, request);
|
|
34
|
+
return request.status;
|
|
35
|
+
}
|
|
@@ -10,7 +10,6 @@ const MISSING_PATH_ERROR_CODE = {
|
|
|
10
10
|
} as const;
|
|
11
11
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
12
12
|
|
|
13
|
-
// File guards must resolve the model's path exactly as Pi's built-in tools do.
|
|
14
13
|
export function resolveFileToolPath(
|
|
15
14
|
cwd: string,
|
|
16
15
|
path: string,
|
|
@@ -29,8 +28,6 @@ export function resolveFileToolPath(
|
|
|
29
28
|
return realpathOr(absolutePath);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
// A file that does not exist yet keeps its unresolved path, so it is matched
|
|
33
|
-
// against the literal working directory as well as the canonical one.
|
|
34
31
|
export function projectRelativePath(
|
|
35
32
|
cwd: string,
|
|
36
33
|
absolutePath: string,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
// A missing file is an expected answer to "what was there before", not a
|
|
4
|
-
// failure. Anything else about the read is a real problem and surfaces.
|
|
5
3
|
export function readFileIfExists(path: string): string | undefined {
|
|
6
4
|
try {
|
|
7
5
|
return readFileSync(path, 'utf8');
|
|
@@ -10,8 +10,59 @@ import type {
|
|
|
10
10
|
const COMPLETED_OUTCOME = 'completed';
|
|
11
11
|
const CUSTOM_MESSAGE_ENTRY = 'custom_message';
|
|
12
12
|
const SETTLE_PRIORITY_EVENT = 'sideroom:settle-priority';
|
|
13
|
+
const SETTLE_SUPPLEMENT_EVENT = 'sideroom:settle-supplement';
|
|
14
|
+
const SETTLE_WAIT_EVENT = 'sideroom:settle-wait';
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
interface SettleWaitRequest {
|
|
17
|
+
readonly ctx: ExtensionContext;
|
|
18
|
+
readonly waits: Promise<void>[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registerSettleWait(
|
|
22
|
+
pi: ExtensionAPI,
|
|
23
|
+
wait: (ctx: ExtensionContext) => Promise<void>,
|
|
24
|
+
): void {
|
|
25
|
+
pi.events.on(SETTLE_WAIT_EVENT, (payload) => {
|
|
26
|
+
const request = payload as SettleWaitRequest;
|
|
27
|
+
request.waits.push(wait(request.ctx));
|
|
28
|
+
});
|
|
29
|
+
pi.on('agent_before_settle', async (event, ctx) => {
|
|
30
|
+
if (acceptsSettleSteer(event)) {
|
|
31
|
+
await wait(ctx);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function waitForSettle(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
37
|
+
const request: SettleWaitRequest = { ctx, waits: [] };
|
|
38
|
+
pi.events.emit(SETTLE_WAIT_EVENT, request);
|
|
39
|
+
await Promise.all(request.waits);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SETTLE_PRIORITY = { review: 0, persona: 1 } as const;
|
|
43
|
+
|
|
44
|
+
interface SettleSupplement {
|
|
45
|
+
readonly customType: string;
|
|
46
|
+
readonly content: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SettleSupplementRequest {
|
|
50
|
+
readonly ctx: ExtensionContext;
|
|
51
|
+
readonly messages: SettleSupplement[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function registerSettleSupplement(
|
|
55
|
+
pi: ExtensionAPI,
|
|
56
|
+
prepare: (ctx: ExtensionContext) => SettleSupplement | undefined,
|
|
57
|
+
): void {
|
|
58
|
+
pi.events.on(SETTLE_SUPPLEMENT_EVENT, (payload) => {
|
|
59
|
+
const request = payload as SettleSupplementRequest;
|
|
60
|
+
const message = prepare(request.ctx);
|
|
61
|
+
if (message !== undefined) {
|
|
62
|
+
request.messages.push(message);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
15
66
|
|
|
16
67
|
interface SettlePriorityRequest {
|
|
17
68
|
readonly ctx: ExtensionContext;
|
|
@@ -31,8 +82,6 @@ const USER_INPUT_SOURCES: ReadonlySet<InputSource> = new Set([
|
|
|
31
82
|
'rpc',
|
|
32
83
|
]);
|
|
33
84
|
|
|
34
|
-
// A message typed while the agent runs joins the current run as a steer or
|
|
35
|
-
// follow-up; only input that arrives idle opens a new turn.
|
|
36
85
|
export function startsUserTurn(event: InputEvent): boolean {
|
|
37
86
|
return (
|
|
38
87
|
USER_INPUT_SOURCES.has(event.source) &&
|
|
@@ -40,8 +89,6 @@ export function startsUserTurn(event: InputEvent): boolean {
|
|
|
40
89
|
);
|
|
41
90
|
}
|
|
42
91
|
|
|
43
|
-
// Any queued message or an earlier handler's continuation still has work to
|
|
44
|
-
// run. Yield without consuming the guard's state until a quiet boundary.
|
|
45
92
|
export function acceptsSettleSteer(event: AgentBeforeSettleEvent): boolean {
|
|
46
93
|
return (
|
|
47
94
|
event.outcome === COMPLETED_OUTCOME &&
|
|
@@ -50,9 +97,6 @@ export function acceptsSettleSteer(event: AgentBeforeSettleEvent): boolean {
|
|
|
50
97
|
);
|
|
51
98
|
}
|
|
52
99
|
|
|
53
|
-
// Pi gives each extension its own API object. Its synchronous event bus is
|
|
54
|
-
// shared, so ask which stages are pending rather than relying on load order
|
|
55
|
-
// or module-global state that would leak across sessions and reloads.
|
|
56
100
|
export function registerSettleGuard(
|
|
57
101
|
pi: ExtensionAPI,
|
|
58
102
|
guard: SettleGuard,
|
|
@@ -64,8 +108,12 @@ export function registerSettleGuard(
|
|
|
64
108
|
}
|
|
65
109
|
});
|
|
66
110
|
|
|
67
|
-
pi.on('agent_before_settle', (event, ctx) => {
|
|
68
|
-
if (!acceptsSettleSteer(event)
|
|
111
|
+
pi.on('agent_before_settle', async (event, ctx) => {
|
|
112
|
+
if (!acceptsSettleSteer(event)) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await waitForSettle(pi, ctx);
|
|
116
|
+
if (ctx.signal?.aborted || ctx.hasPendingMessages() || !acceptsSettleSteer(event) || !guard.pending(ctx)) {
|
|
69
117
|
return;
|
|
70
118
|
}
|
|
71
119
|
const request: SettlePriorityRequest = { ctx, priorities: new Set() };
|
|
@@ -73,12 +121,29 @@ export function registerSettleGuard(
|
|
|
73
121
|
if ([...request.priorities].some((priority) => priority < guard.priority)) {
|
|
74
122
|
return;
|
|
75
123
|
}
|
|
76
|
-
|
|
124
|
+
const continuation = guard.handle(event, ctx);
|
|
125
|
+
if (!continuation?.continue) {
|
|
126
|
+
return continuation;
|
|
127
|
+
}
|
|
128
|
+
const supplements: SettleSupplementRequest = { ctx, messages: [] };
|
|
129
|
+
pi.events.emit(SETTLE_SUPPLEMENT_EVENT, supplements);
|
|
130
|
+
return {
|
|
131
|
+
...continuation,
|
|
132
|
+
entries: [
|
|
133
|
+
...(continuation.entries ?? event.entries),
|
|
134
|
+
...supplements.messages.map(
|
|
135
|
+
(message) =>
|
|
136
|
+
({
|
|
137
|
+
type: CUSTOM_MESSAGE_ENTRY,
|
|
138
|
+
...message,
|
|
139
|
+
display: false,
|
|
140
|
+
}) as const,
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
};
|
|
77
144
|
});
|
|
78
145
|
}
|
|
79
146
|
|
|
80
|
-
// Handlers chain on agent_before_settle, so keep the entries earlier handlers
|
|
81
|
-
// proposed; replacing them would drop another extension's steer.
|
|
82
147
|
interface HiddenSteerOptions {
|
|
83
148
|
readonly event: AgentBeforeSettleEvent;
|
|
84
149
|
readonly customType: string;
|
|
@@ -1,109 +1,65 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionAPI,
|
|
3
|
-
ExtensionContext,
|
|
4
|
-
} from '@earendil-works/pi-coding-agent';
|
|
5
1
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
isEditToolResult,
|
|
5
|
+
isWriteToolResult,
|
|
6
|
+
} from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { startsUserTurn } from '../shared/settle.ts';
|
|
8
|
+
import { TODO_STATUS } from './model.ts';
|
|
10
9
|
import type { TodoStore } from './session.ts';
|
|
11
10
|
|
|
12
|
-
const
|
|
13
|
-
const WATCHDOG_TYPE = 'sideroom-todo-watchdog';
|
|
14
|
-
const MUTATING_TOOL_NAMES = new Set(['write', 'edit', 'bash']);
|
|
15
|
-
|
|
16
|
-
interface TodoRunState {
|
|
17
|
-
proposeNudgedThisRun: boolean;
|
|
18
|
-
updateWatchdogThisTurn: boolean;
|
|
19
|
-
turnMutated: boolean;
|
|
20
|
-
turnCalledTodo: boolean;
|
|
21
|
-
steerFromUs: boolean;
|
|
22
|
-
}
|
|
11
|
+
const REMINDER_TYPE = 'sideroom-todo-reminder';
|
|
23
12
|
|
|
24
13
|
export function registerGuardEvents(
|
|
25
14
|
pi: ExtensionAPI,
|
|
26
15
|
store: TodoStore,
|
|
27
16
|
restore: (ctx: ExtensionContext) => void,
|
|
28
17
|
): void {
|
|
29
|
-
|
|
18
|
+
let remindedThisTurn = false;
|
|
30
19
|
|
|
31
20
|
pi.on('input', (event) => {
|
|
32
|
-
if (event
|
|
33
|
-
|
|
34
|
-
runState.steerFromUs = false;
|
|
21
|
+
if (startsUserTurn(event)) {
|
|
22
|
+
remindedThisTurn = false;
|
|
35
23
|
}
|
|
36
24
|
});
|
|
25
|
+
pi.on('session_start', () => {
|
|
26
|
+
remindedThisTurn = false;
|
|
27
|
+
});
|
|
37
28
|
|
|
38
|
-
pi.on('
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
pi.on('tool_result', (event, ctx) => {
|
|
30
|
+
if (
|
|
31
|
+
remindedThisTurn ||
|
|
32
|
+
event.isError ||
|
|
33
|
+
(!isWriteToolResult(event) && !isEditToolResult(event))
|
|
34
|
+
) {
|
|
43
35
|
return;
|
|
44
36
|
}
|
|
45
|
-
if (!MUTATING_TOOL_NAMES.has(event.toolName)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
runState.turnMutated = true;
|
|
50
37
|
restore(ctx);
|
|
51
|
-
if (
|
|
38
|
+
if (store.items.length === 0 && ctx.mode !== 'tui') {
|
|
52
39
|
return;
|
|
53
40
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
pi.on('turn_end', (_event, ctx) => {
|
|
65
|
-
restore(ctx);
|
|
66
|
-
if (!shouldWatchdogUpdate({ items: store.items, ...runState })) {
|
|
41
|
+
if (
|
|
42
|
+
store.items.length > 0 &&
|
|
43
|
+
!store.items.some(
|
|
44
|
+
(item) =>
|
|
45
|
+
item.status === TODO_STATUS.pending ||
|
|
46
|
+
item.status === TODO_STATUS.inProgress,
|
|
47
|
+
)
|
|
48
|
+
) {
|
|
67
49
|
return;
|
|
68
50
|
}
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
52
|
+
remindedThisTurn = true;
|
|
53
|
+
pi.sendMessage(
|
|
54
|
+
{
|
|
55
|
+
customType: REMINDER_TYPE,
|
|
56
|
+
content:
|
|
57
|
+
store.items.length === 0
|
|
58
|
+
? 'Use sideroom_todo propose if this implementation has multiple steps. Maintain the board at task milestones, not after every tool call.'
|
|
59
|
+
: 'Update sideroom_todo when a task changes status, combining completion and the next task in one call. Do not update merely because a tool ran.',
|
|
60
|
+
display: false,
|
|
61
|
+
},
|
|
62
|
+
{ triggerTurn: false, deliverAs: 'steer' },
|
|
76
63
|
);
|
|
77
64
|
});
|
|
78
65
|
}
|
|
79
|
-
|
|
80
|
-
function createTodoRunState(): TodoRunState {
|
|
81
|
-
return {
|
|
82
|
-
proposeNudgedThisRun: false,
|
|
83
|
-
updateWatchdogThisTurn: false,
|
|
84
|
-
turnMutated: false,
|
|
85
|
-
turnCalledTodo: false,
|
|
86
|
-
steerFromUs: false,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function resetTurnState(runState: TodoRunState): void {
|
|
91
|
-
runState.turnMutated = false;
|
|
92
|
-
runState.turnCalledTodo = false;
|
|
93
|
-
runState.updateWatchdogThisTurn = false;
|
|
94
|
-
// A steer only silences the turn that sent it; later turns may steer again.
|
|
95
|
-
runState.steerFromUs = false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function sendSteer(
|
|
99
|
-
pi: ExtensionAPI,
|
|
100
|
-
runState: TodoRunState,
|
|
101
|
-
customType: string,
|
|
102
|
-
content: string,
|
|
103
|
-
): void {
|
|
104
|
-
runState.steerFromUs = true;
|
|
105
|
-
pi.sendMessage(
|
|
106
|
-
{ customType, content, display: false },
|
|
107
|
-
{ triggerTurn: true, deliverAs: 'steer' },
|
|
108
|
-
);
|
|
109
|
-
}
|
package/extensions/todo/index.ts
CHANGED
|
@@ -29,13 +29,12 @@ export const TODO_PROMPT_SNIPPET =
|
|
|
29
29
|
export const TODO_PROMPT_GUIDELINES = [
|
|
30
30
|
'Use sideroom_todo to maintain a live work board when implementation has more than one step.',
|
|
31
31
|
'While any item is pending, exactly one must be in_progress.',
|
|
32
|
-
'Complete the current item and start the next item in the same update call.',
|
|
32
|
+
'Update at task milestones, not after each tool call. Complete the current item and start the next item in the same update call.',
|
|
33
33
|
'propose is interactive-TUI-only; update also works in print, JSON, and RPC modes.',
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
const BOARD_SHORTCUT = 'f9';
|
|
37
37
|
|
|
38
|
-
// Pi loads extensions/*/index.ts through export default.
|
|
39
38
|
export default function registerTodo(pi: ExtensionAPI): void {
|
|
40
39
|
const store = createTodoStore();
|
|
41
40
|
const overlay = createTodoBoardOverlayController(() => store.items);
|
package/extensions/todo/model.ts
CHANGED
|
@@ -66,25 +66,11 @@ const UpdateParamsSchema = Type.Object({
|
|
|
66
66
|
patches: TodoPatchesField,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Strict validator and types for sideroom_todo arguments. Kept as a
|
|
71
|
-
* discriminated union so invalid action/field combinations are rejected
|
|
72
|
-
* exactly. This schema is not registered with Pi; see
|
|
73
|
-
* TodoToolParametersSchema.
|
|
74
|
-
*/
|
|
75
69
|
export const TodoParamsSchema = Type.Union([
|
|
76
70
|
ProposeParamsSchema,
|
|
77
71
|
UpdateParamsSchema,
|
|
78
72
|
]);
|
|
79
73
|
|
|
80
|
-
/**
|
|
81
|
-
* Tool-facing schema registered with Pi. It must stay a top-level object:
|
|
82
|
-
* Claude Code silently drops every tool of an MCP server when one tool's
|
|
83
|
-
* inputSchema has a non-object top level (anyOf/oneOf/allOf), which is exactly
|
|
84
|
-
* what TodoParamsSchema emits. The discriminated union is still enforced at
|
|
85
|
-
* runtime by parseTodoParams. See docs/extensions/todo.md ("Tool schema
|
|
86
|
-
* compatibility") for the bug references and the condition to revert.
|
|
87
|
-
*/
|
|
88
74
|
export const TodoToolParametersSchema = Type.Object({
|
|
89
75
|
action: TodoActionSchema,
|
|
90
76
|
items: Type.Optional(TodoItemsField),
|
|
@@ -110,13 +96,13 @@ export interface TodoPatch {
|
|
|
110
96
|
|
|
111
97
|
export type ParsedTodoParams =
|
|
112
98
|
| {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
99
|
+
readonly action: typeof TODO_ACTION.propose;
|
|
100
|
+
readonly items: readonly TodoItem[];
|
|
101
|
+
}
|
|
116
102
|
| {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
readonly action: typeof TODO_ACTION.update;
|
|
104
|
+
readonly patches: readonly TodoPatch[];
|
|
105
|
+
};
|
|
120
106
|
|
|
121
107
|
export type ParseTodoResult =
|
|
122
108
|
| { readonly ok: true; readonly params: ParsedTodoParams }
|
|
@@ -126,15 +112,6 @@ export type ApplyTodoResult =
|
|
|
126
112
|
| { readonly ok: true; readonly items: readonly TodoItem[] }
|
|
127
113
|
| { readonly ok: false; readonly message: string };
|
|
128
114
|
|
|
129
|
-
export interface TodoGuardState {
|
|
130
|
-
readonly items: readonly TodoItem[];
|
|
131
|
-
readonly turnMutated: boolean;
|
|
132
|
-
readonly turnCalledTodo: boolean;
|
|
133
|
-
readonly proposeNudgedThisRun: boolean;
|
|
134
|
-
readonly updateWatchdogThisTurn: boolean;
|
|
135
|
-
readonly steerFromUs: boolean;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
115
|
const STATUS_MARKS: Readonly<Record<TodoStatus, string>> = {
|
|
139
116
|
[TODO_STATUS.pending]: '-',
|
|
140
117
|
[TODO_STATUS.inProgress]: '>',
|
|
@@ -293,25 +270,6 @@ export function formatBoardBlock(items: readonly TodoItem[]): string {
|
|
|
293
270
|
].join('\n');
|
|
294
271
|
}
|
|
295
272
|
|
|
296
|
-
export function shouldNudgePropose(state: TodoGuardState): boolean {
|
|
297
|
-
return (
|
|
298
|
-
state.turnMutated &&
|
|
299
|
-
state.items.length === 0 &&
|
|
300
|
-
!state.proposeNudgedThisRun &&
|
|
301
|
-
!state.steerFromUs
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export function shouldWatchdogUpdate(state: TodoGuardState): boolean {
|
|
306
|
-
return (
|
|
307
|
-
state.turnMutated &&
|
|
308
|
-
state.items.length > 0 &&
|
|
309
|
-
!state.turnCalledTodo &&
|
|
310
|
-
!state.updateWatchdogThisTurn &&
|
|
311
|
-
!state.steerFromUs
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
273
|
function normalizeItems(
|
|
316
274
|
items: readonly Static<typeof TodoItemSchema>[],
|
|
317
275
|
):
|
|
@@ -94,8 +94,7 @@ export function registerSessionRefreshEvents(
|
|
|
94
94
|
store: TodoStore,
|
|
95
95
|
restoreAndRefresh: BoardLifecycle,
|
|
96
96
|
): void {
|
|
97
|
-
|
|
98
|
-
// board still has to be announced again.
|
|
97
|
+
|
|
99
98
|
const restoreSession = (ctx: ExtensionContext) => {
|
|
100
99
|
restoreAndRefresh(ctx);
|
|
101
100
|
forgetSentBoardBlock(store);
|
|
@@ -132,8 +131,6 @@ export function registerBoardContext(
|
|
|
132
131
|
});
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
// The block rides a session message instead of the system prompt: a volatile
|
|
136
|
-
// system prompt invalidates the cached prefix of the whole request.
|
|
137
134
|
function pendingBoardBlock(store: TodoStore): string | undefined {
|
|
138
135
|
if (store.items.length === 0) {
|
|
139
136
|
return undefined;
|
|
@@ -176,17 +176,17 @@ export async function startBroker({
|
|
|
176
176
|
delivery.browser,
|
|
177
177
|
typeof message.error === 'string' || stale
|
|
178
178
|
? {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
type: MESSAGE_TYPE.error,
|
|
180
|
+
id: delivery.id,
|
|
181
|
+
message: stale
|
|
182
|
+
? 'The session changed. Select the element again.'
|
|
183
|
+
: String(message.error).slice(0, 500),
|
|
184
|
+
}
|
|
185
185
|
: {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
type: MESSAGE_TYPE.received,
|
|
187
|
+
id: delivery.id,
|
|
188
|
+
candidates: message.candidates,
|
|
189
|
+
},
|
|
190
190
|
);
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
@@ -102,8 +102,8 @@ function staticObject(expression: unknown, document: Document): ConfigObject {
|
|
|
102
102
|
entry.type === 'ExportNamedDeclaration' ? entry.declaration : entry;
|
|
103
103
|
return statement?.type === 'VariableDeclaration'
|
|
104
104
|
? statement.declarations.flatMap((item) =>
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
item === declaration ? [] : [item.init],
|
|
106
|
+
)
|
|
107
107
|
: [];
|
|
108
108
|
});
|
|
109
109
|
if (
|
|
@@ -338,7 +338,7 @@ export function configureSource({
|
|
|
338
338
|
if (value.start === null || value.start === undefined) {
|
|
339
339
|
throw new Error('The plugin array location is unavailable.');
|
|
340
340
|
}
|
|
341
|
-
|
|
341
|
+
|
|
342
342
|
replacements.push({
|
|
343
343
|
start: value.start + 1,
|
|
344
344
|
end: value.start + 1,
|
|
@@ -415,7 +415,7 @@ export function configureSource({
|
|
|
415
415
|
replacement.text +
|
|
416
416
|
configured.slice(replacement.end);
|
|
417
417
|
}
|
|
418
|
-
|
|
418
|
+
|
|
419
419
|
parse(configured, { sourceType: 'unambiguous', plugins: ['typescript'] });
|
|
420
420
|
return { source: configured };
|
|
421
421
|
}
|
|
@@ -67,8 +67,6 @@ export interface VisualPathPolicy {
|
|
|
67
67
|
readonly allowedPath: (path: string) => string | undefined;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Reuse ignore rules only within one synchronous operation. File metadata is
|
|
71
|
-
// always read again: a remembered path must not authorize a replacement symlink.
|
|
72
70
|
export function createPathPolicy(cwd: string): VisualPathPolicy {
|
|
73
71
|
const root = realpathSync(cwd);
|
|
74
72
|
const policies = new Map<
|
|
@@ -133,7 +131,6 @@ export function createPathPolicy(cwd: string): VisualPathPolicy {
|
|
|
133
131
|
}
|
|
134
132
|
}
|
|
135
133
|
|
|
136
|
-
// Browser paths are untrusted, including paths reported by compiler plugins.
|
|
137
134
|
export function allowedPath(cwd: string, path: string): string | undefined {
|
|
138
135
|
return createPathPolicy(cwd).allowedPath(path);
|
|
139
136
|
}
|
|
@@ -71,7 +71,7 @@ export default function registerVisualContext(
|
|
|
71
71
|
commitPending({ store, pi, pending });
|
|
72
72
|
refresh(ctx);
|
|
73
73
|
};
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
const dialogIsCurrent = (ctx: ExtensionContext, epoch: number): boolean =>
|
|
76
76
|
epoch === generation && activeContext === ctx;
|
|
77
77
|
async function activate(ctx: ExtensionContext): Promise<void> {
|
|
@@ -191,8 +191,7 @@ export default function registerVisualContext(
|
|
|
191
191
|
images: event.images,
|
|
192
192
|
};
|
|
193
193
|
});
|
|
194
|
-
|
|
195
|
-
// the run actually starts so a failed submission cannot lose the attachment.
|
|
194
|
+
|
|
196
195
|
pi.on('before_agent_start', (event, ctx) => {
|
|
197
196
|
const attachment = preparedContext;
|
|
198
197
|
preparedContext = undefined;
|
|
@@ -304,13 +303,13 @@ export default function registerVisualContext(
|
|
|
304
303
|
const pending =
|
|
305
304
|
selected.action === 'remove'
|
|
306
305
|
? store.pending.filter(
|
|
307
|
-
|
|
308
|
-
|
|
306
|
+
(item) => item.selection.id !== entry.selection.id,
|
|
307
|
+
)
|
|
309
308
|
: store.pending.map((item) =>
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
item.selection.id === entry.selection.id
|
|
310
|
+
? { ...item, chosen: candidate }
|
|
311
|
+
: item,
|
|
312
|
+
);
|
|
314
313
|
savePending(ctx, pending);
|
|
315
314
|
} finally {
|
|
316
315
|
opening = false;
|
|
@@ -13,7 +13,6 @@ export function isBabelSyntaxError(cause: unknown): boolean {
|
|
|
13
13
|
);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Only syntax literals are read. No inspected source is imported or executed.
|
|
17
16
|
export function literalText(expression: string): string | undefined {
|
|
18
17
|
let node: ReturnType<typeof parseExpression>;
|
|
19
18
|
try {
|
|
@@ -12,9 +12,9 @@ export type SourceNode =
|
|
|
12
12
|
| SourceElement
|
|
13
13
|
| { readonly kind: 'text'; readonly value: string }
|
|
14
14
|
| {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
readonly kind: 'group' | 'dynamic';
|
|
16
|
+
readonly children: readonly SourceNode[];
|
|
17
|
+
};
|
|
18
18
|
|
|
19
19
|
export interface ParsedMarkup {
|
|
20
20
|
readonly nodes: readonly SourceNode[];
|
|
@@ -81,8 +81,6 @@ interface ElementRangeInput {
|
|
|
81
81
|
readonly tag: string;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Parsers may recover malformed syntax or omit locations. Such nodes must not
|
|
85
|
-
// silently become source claims, even if their descriptors look convincing.
|
|
86
84
|
export function verifiedElementRange({
|
|
87
85
|
source,
|
|
88
86
|
start,
|
|
@@ -118,9 +118,9 @@ export interface SourceCandidate {
|
|
|
118
118
|
|
|
119
119
|
export interface SourceResolution {
|
|
120
120
|
readonly candidates: readonly SourceCandidate[];
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
readonly truncated: boolean;
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
readonly capped: boolean;
|
|
125
125
|
readonly issue?: string;
|
|
126
126
|
}
|