shortcutxl 0.2.12 → 0.2.13

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.
Files changed (110) hide show
  1. package/README.md +26 -26
  2. package/agent-docs/README.md +397 -397
  3. package/agent-docs/docs/compaction.md +390 -390
  4. package/agent-docs/docs/custom-provider.md +580 -580
  5. package/agent-docs/docs/extensions.md +1971 -1971
  6. package/agent-docs/docs/packages.md +209 -209
  7. package/agent-docs/docs/rpc.md +1317 -1317
  8. package/agent-docs/docs/sdk.md +962 -962
  9. package/agent-docs/docs/session.md +412 -412
  10. package/agent-docs/docs/termux.md +127 -127
  11. package/agent-docs/docs/tui.md +887 -887
  12. package/agent-docs/examples/README.md +25 -25
  13. package/agent-docs/examples/extensions/README.md +205 -205
  14. package/agent-docs/examples/extensions/antigravity-image-gen.ts +447 -447
  15. package/agent-docs/examples/extensions/auto-commit-on-exit.ts +49 -49
  16. package/agent-docs/examples/extensions/bash-spawn-hook.ts +30 -30
  17. package/agent-docs/examples/extensions/bookmark.ts +50 -50
  18. package/agent-docs/examples/extensions/built-in-tool-renderer.ts +256 -256
  19. package/agent-docs/examples/extensions/claude-rules.ts +86 -86
  20. package/agent-docs/examples/extensions/commands.ts +75 -75
  21. package/agent-docs/examples/extensions/confirm-destructive.ts +59 -59
  22. package/agent-docs/examples/extensions/custom-compaction.ts +126 -126
  23. package/agent-docs/examples/extensions/custom-footer.ts +63 -63
  24. package/agent-docs/examples/extensions/custom-header.ts +73 -73
  25. package/agent-docs/examples/extensions/custom-provider-anthropic/index.ts +660 -660
  26. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/index.ts +362 -362
  27. package/agent-docs/examples/extensions/custom-provider-gitlab-duo/test.ts +88 -88
  28. package/agent-docs/examples/extensions/custom-provider-qwen-cli/index.ts +349 -349
  29. package/agent-docs/examples/extensions/dirty-repo-guard.ts +56 -56
  30. package/agent-docs/examples/extensions/doom-overlay/doom-component.ts +133 -133
  31. package/agent-docs/examples/extensions/doom-overlay/doom-keys.ts +108 -108
  32. package/agent-docs/examples/extensions/doom-overlay/index.ts +74 -74
  33. package/agent-docs/examples/extensions/dynamic-resources/index.ts +15 -15
  34. package/agent-docs/examples/extensions/dynamic-tools.ts +77 -77
  35. package/agent-docs/examples/extensions/event-bus.ts +43 -43
  36. package/agent-docs/examples/extensions/file-trigger.ts +41 -41
  37. package/agent-docs/examples/extensions/git-checkpoint.ts +53 -53
  38. package/agent-docs/examples/extensions/handoff.ts +155 -155
  39. package/agent-docs/examples/extensions/hello.ts +25 -25
  40. package/agent-docs/examples/extensions/inline-bash.ts +94 -94
  41. package/agent-docs/examples/extensions/input-transform.ts +43 -43
  42. package/agent-docs/examples/extensions/interactive-shell.ts +209 -209
  43. package/agent-docs/examples/extensions/mac-system-theme.ts +47 -47
  44. package/agent-docs/examples/extensions/message-renderer.ts +59 -59
  45. package/agent-docs/examples/extensions/minimal-mode.ts +430 -430
  46. package/agent-docs/examples/extensions/modal-editor.ts +90 -90
  47. package/agent-docs/examples/extensions/model-status.ts +31 -31
  48. package/agent-docs/examples/extensions/notify.ts +55 -55
  49. package/agent-docs/examples/extensions/overlay-qa-tests.ts +936 -936
  50. package/agent-docs/examples/extensions/overlay-test.ts +159 -159
  51. package/agent-docs/examples/extensions/permission-gate.ts +37 -37
  52. package/agent-docs/examples/extensions/pirate.ts +47 -47
  53. package/agent-docs/examples/extensions/plan-mode/index.ts +363 -363
  54. package/agent-docs/examples/extensions/preset.ts +418 -418
  55. package/agent-docs/examples/extensions/protected-paths.ts +30 -30
  56. package/agent-docs/examples/extensions/qna.ts +122 -122
  57. package/agent-docs/examples/extensions/question.ts +278 -278
  58. package/agent-docs/examples/extensions/questionnaire.ts +440 -440
  59. package/agent-docs/examples/extensions/rainbow-editor.ts +90 -90
  60. package/agent-docs/examples/extensions/reload-runtime.ts +37 -37
  61. package/agent-docs/examples/extensions/rpc-demo.ts +124 -124
  62. package/agent-docs/examples/extensions/sandbox/index.ts +324 -324
  63. package/agent-docs/examples/extensions/send-user-message.ts +97 -97
  64. package/agent-docs/examples/extensions/session-name.ts +27 -27
  65. package/agent-docs/examples/extensions/shutdown-command.ts +69 -69
  66. package/agent-docs/examples/extensions/snake.ts +343 -343
  67. package/agent-docs/examples/extensions/space-invaders.ts +566 -566
  68. package/agent-docs/examples/extensions/ssh.ts +233 -233
  69. package/agent-docs/examples/extensions/status-line.ts +40 -40
  70. package/agent-docs/examples/extensions/subagent/agents.ts +130 -130
  71. package/agent-docs/examples/extensions/subagent/index.ts +1068 -1068
  72. package/agent-docs/examples/extensions/summarize.ts +206 -206
  73. package/agent-docs/examples/extensions/system-prompt-header.ts +17 -17
  74. package/agent-docs/examples/extensions/timed-confirm.ts +72 -72
  75. package/agent-docs/examples/extensions/titlebar-spinner.ts +58 -58
  76. package/agent-docs/examples/extensions/todo.ts +314 -314
  77. package/agent-docs/examples/extensions/tool-override.ts +146 -146
  78. package/agent-docs/examples/extensions/tools.ts +145 -145
  79. package/agent-docs/examples/extensions/trigger-compact.ts +40 -40
  80. package/agent-docs/examples/extensions/truncated-tool.ts +194 -194
  81. package/agent-docs/examples/extensions/widget-placement.ts +17 -17
  82. package/agent-docs/examples/extensions/with-deps/index.ts +37 -37
  83. package/agent-docs/examples/rpc-extension-ui.ts +654 -654
  84. package/agent-docs/examples/sdk/01-minimal.ts +22 -22
  85. package/agent-docs/examples/sdk/02-custom-model.ts +48 -48
  86. package/agent-docs/examples/sdk/03-custom-prompt.ts +55 -55
  87. package/agent-docs/examples/sdk/04-skills.ts +53 -53
  88. package/agent-docs/examples/sdk/05-tools.ts +56 -56
  89. package/agent-docs/examples/sdk/06-extensions.ts +88 -88
  90. package/agent-docs/examples/sdk/07-context-files.ts +40 -40
  91. package/agent-docs/examples/sdk/08-prompt-templates.ts +47 -47
  92. package/agent-docs/examples/sdk/09-api-keys-and-oauth.ts +48 -48
  93. package/agent-docs/examples/sdk/10-settings.ts +54 -54
  94. package/agent-docs/examples/sdk/11-sessions.ts +48 -48
  95. package/agent-docs/examples/sdk/12-full-control.ts +82 -82
  96. package/agent-docs/examples/sdk/README.md +144 -144
  97. package/agent-docs/xll-spec.md +110 -110
  98. package/dist/core/auth-storage.js +21 -2
  99. package/package.json +1 -1
  100. package/xll/ShortcutXL.xll +0 -0
  101. package/xll/modules/debug_render.py +272 -272
  102. package/xll/modules/gameboy.py +241 -241
  103. package/xll/modules/pong.py +188 -188
  104. package/xll/modules/shortcut_xl/_diff_highlight.py +176 -0
  105. package/xll/modules/shortcut_xl/_log.py +12 -12
  106. package/xll/modules/shortcut_xl/_registry.py +44 -44
  107. package/xll/modules/stocks.py +100 -100
  108. /package/skills/{com-advanced-api → COM-advanced-api}/SKILL.md +0 -0
  109. /package/skills/{com-advanced-api → COM-advanced-api}/excel-type-library.py +0 -0
  110. /package/skills/{com-advanced-api → COM-advanced-api}/office-type-library.py +0 -0
@@ -1,363 +1,363 @@
1
- /**
2
- * Plan Mode Extension
3
- *
4
- * Read-only exploration mode for safe code analysis.
5
- * When enabled, only read-only tools are available.
6
- *
7
- * Features:
8
- * - /plan command or Ctrl+Alt+P to toggle
9
- * - Bash restricted to allowlisted read-only commands
10
- * - Extracts numbered plan steps from "Plan:" sections
11
- * - [DONE:n] markers to complete steps during execution
12
- * - Progress tracking widget during execution
13
- */
14
-
15
- import type {
16
- AgentMessage,
17
- AssistantMessage,
18
- ExtensionAPI,
19
- ExtensionContext,
20
- TextContent
21
- } from 'shortcutxl';
22
- import { Key } from 'shortcutxl';
23
- import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from './utils.js';
24
-
25
- // Tools
26
- const PLAN_MODE_TOOLS = ['read', 'bash', 'grep', 'find', 'ls', 'questionnaire'];
27
- const NORMAL_MODE_TOOLS = ['read', 'bash', 'edit', 'write'];
28
-
29
- // Type guard for assistant messages
30
- function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
31
- return m.role === 'assistant' && Array.isArray(m.content);
32
- }
33
-
34
- // Extract text content from an assistant message
35
- function getTextContent(message: AssistantMessage): string {
36
- return message.content
37
- .filter((block): block is TextContent => block.type === 'text')
38
- .map((block) => block.text)
39
- .join('\n');
40
- }
41
-
42
- export default function planModeExtension(shortcut: ExtensionAPI): void {
43
- let planModeEnabled = false;
44
- let executionMode = false;
45
- let todoItems: TodoItem[] = [];
46
-
47
- shortcut.registerFlag('plan', {
48
- description: 'Start in plan mode (read-only exploration)',
49
- type: 'boolean',
50
- default: false
51
- });
52
-
53
- function updateStatus(ctx: ExtensionContext): void {
54
- // Footer status
55
- if (executionMode && todoItems.length > 0) {
56
- const completed = todoItems.filter((t) => t.completed).length;
57
- ctx.ui.setStatus(
58
- 'plan-mode',
59
- ctx.ui.theme.fg('accent', `📋 ${completed}/${todoItems.length}`)
60
- );
61
- } else if (planModeEnabled) {
62
- ctx.ui.setStatus('plan-mode', ctx.ui.theme.fg('warning', '⏸ plan'));
63
- } else {
64
- ctx.ui.setStatus('plan-mode', undefined);
65
- }
66
-
67
- // Widget showing todo list
68
- if (executionMode && todoItems.length > 0) {
69
- const lines = todoItems.map((item) => {
70
- if (item.completed) {
71
- return (
72
- ctx.ui.theme.fg('success', '☑ ') +
73
- ctx.ui.theme.fg('muted', ctx.ui.theme.strikethrough(item.text))
74
- );
75
- }
76
- return `${ctx.ui.theme.fg('muted', '☐ ')}${item.text}`;
77
- });
78
- ctx.ui.setWidget('plan-todos', lines);
79
- } else {
80
- ctx.ui.setWidget('plan-todos', undefined);
81
- }
82
- }
83
-
84
- function togglePlanMode(ctx: ExtensionContext): void {
85
- planModeEnabled = !planModeEnabled;
86
- executionMode = false;
87
- todoItems = [];
88
-
89
- if (planModeEnabled) {
90
- shortcut.setActiveTools(PLAN_MODE_TOOLS);
91
- ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(', ')}`);
92
- } else {
93
- shortcut.setActiveTools(NORMAL_MODE_TOOLS);
94
- ctx.ui.notify('Plan mode disabled. Full access restored.');
95
- }
96
- updateStatus(ctx);
97
- }
98
-
99
- function persistState(): void {
100
- shortcut.appendEntry('plan-mode', {
101
- enabled: planModeEnabled,
102
- todos: todoItems,
103
- executing: executionMode
104
- });
105
- }
106
-
107
- shortcut.registerCommand('plan', {
108
- description: 'Toggle plan mode (read-only exploration)',
109
- handler: async (_args, ctx) => togglePlanMode(ctx)
110
- });
111
-
112
- shortcut.registerCommand('todos', {
113
- description: 'Show current plan todo list',
114
- handler: async (_args, ctx) => {
115
- if (todoItems.length === 0) {
116
- ctx.ui.notify('No todos. Create a plan first with /plan', 'info');
117
- return;
118
- }
119
- const list = todoItems
120
- .map((item, i) => `${i + 1}. ${item.completed ? '✓' : '○'} ${item.text}`)
121
- .join('\n');
122
- ctx.ui.notify(`Plan Progress:\n${list}`, 'info');
123
- }
124
- });
125
-
126
- shortcut.registerShortcut(Key.ctrlAlt('p'), {
127
- description: 'Toggle plan mode',
128
- handler: async (ctx) => togglePlanMode(ctx)
129
- });
130
-
131
- // Block destructive bash commands in plan mode
132
- shortcut.on('tool_call', async (event) => {
133
- if (!planModeEnabled || event.toolName !== 'bash') return;
134
-
135
- const command = event.input.command as string;
136
- if (!isSafeCommand(command)) {
137
- return {
138
- block: true,
139
- reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`
140
- };
141
- }
142
- });
143
-
144
- // Filter out stale plan mode context when not in plan mode
145
- shortcut.on('context', async (event) => {
146
- if (planModeEnabled) return;
147
-
148
- return {
149
- messages: event.messages.filter((m) => {
150
- const msg = m as AgentMessage & { customType?: string };
151
- if (msg.customType === 'plan-mode-context') return false;
152
- if (msg.role !== 'user') return true;
153
-
154
- const content = msg.content;
155
- if (typeof content === 'string') {
156
- return !content.includes('[PLAN MODE ACTIVE]');
157
- }
158
- if (Array.isArray(content)) {
159
- return !content.some(
160
- (c) => c.type === 'text' && (c as TextContent).text?.includes('[PLAN MODE ACTIVE]')
161
- );
162
- }
163
- return true;
164
- })
165
- };
166
- });
167
-
168
- // Inject plan/execution context before agent starts
169
- shortcut.on('before_agent_start', async () => {
170
- if (planModeEnabled) {
171
- return {
172
- message: {
173
- customType: 'plan-mode-context',
174
- content: `[PLAN MODE ACTIVE]
175
- You are in plan mode - a read-only exploration mode for safe code analysis.
176
-
177
- Restrictions:
178
- - You can only use: read, bash, grep, find, ls, questionnaire
179
- - You CANNOT use: edit, write (file modifications are disabled)
180
- - Bash is restricted to an allowlist of read-only commands
181
-
182
- Ask clarifying questions using the questionnaire tool.
183
- Use brave-search skill via bash for web research.
184
-
185
- Create a detailed numbered plan under a "Plan:" header:
186
-
187
- Plan:
188
- 1. First step description
189
- 2. Second step description
190
- ...
191
-
192
- Do NOT attempt to make changes - just describe what you would do.`,
193
- display: false
194
- }
195
- };
196
- }
197
-
198
- if (executionMode && todoItems.length > 0) {
199
- const remaining = todoItems.filter((t) => !t.completed);
200
- const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join('\n');
201
- return {
202
- message: {
203
- customType: 'plan-execution-context',
204
- content: `[EXECUTING PLAN - Full tool access enabled]
205
-
206
- Remaining steps:
207
- ${todoList}
208
-
209
- Execute each step in order.
210
- After completing a step, include a [DONE:n] tag in your response.`,
211
- display: false
212
- }
213
- };
214
- }
215
- });
216
-
217
- // Track progress after each turn
218
- shortcut.on('turn_end', async (event, ctx) => {
219
- if (!executionMode || todoItems.length === 0) return;
220
- if (!isAssistantMessage(event.message)) return;
221
-
222
- const text = getTextContent(event.message);
223
- if (markCompletedSteps(text, todoItems) > 0) {
224
- updateStatus(ctx);
225
- }
226
- persistState();
227
- });
228
-
229
- // Handle plan completion and plan mode UI
230
- shortcut.on('agent_end', async (event, ctx) => {
231
- // Check if execution is complete
232
- if (executionMode && todoItems.length > 0) {
233
- if (todoItems.every((t) => t.completed)) {
234
- const completedList = todoItems.map((t) => `~~${t.text}~~`).join('\n');
235
- shortcut.sendMessage(
236
- {
237
- customType: 'plan-complete',
238
- content: `**Plan Complete!** ✓\n\n${completedList}`,
239
- display: true
240
- },
241
- { triggerTurn: false }
242
- );
243
- executionMode = false;
244
- todoItems = [];
245
- shortcut.setActiveTools(NORMAL_MODE_TOOLS);
246
- updateStatus(ctx);
247
- persistState(); // Save cleared state so resume doesn't restore old execution mode
248
- }
249
- return;
250
- }
251
-
252
- if (!planModeEnabled || !ctx.hasUI) return;
253
-
254
- // Extract todos from last assistant message
255
- const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
256
- if (lastAssistant) {
257
- const extracted = extractTodoItems(getTextContent(lastAssistant));
258
- if (extracted.length > 0) {
259
- todoItems = extracted;
260
- }
261
- }
262
-
263
- // Show plan steps and prompt for next action
264
- if (todoItems.length > 0) {
265
- const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join('\n');
266
- shortcut.sendMessage(
267
- {
268
- customType: 'plan-todo-list',
269
- content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
270
- display: true
271
- },
272
- { triggerTurn: false }
273
- );
274
- }
275
-
276
- const choice = await ctx.ui.select('Plan mode - what next?', [
277
- todoItems.length > 0 ? 'Execute the plan (track progress)' : 'Execute the plan',
278
- 'Stay in plan mode',
279
- 'Refine the plan'
280
- ]);
281
-
282
- if (choice?.startsWith('Execute')) {
283
- planModeEnabled = false;
284
- executionMode = todoItems.length > 0;
285
- shortcut.setActiveTools(NORMAL_MODE_TOOLS);
286
- updateStatus(ctx);
287
-
288
- const execMessage =
289
- todoItems.length > 0
290
- ? `Execute the plan. Start with: ${todoItems[0].text}`
291
- : 'Execute the plan you just created.';
292
- shortcut.sendMessage(
293
- { customType: 'plan-mode-execute', content: execMessage, display: true },
294
- { triggerTurn: true }
295
- );
296
- } else if (choice === 'Refine the plan') {
297
- const refinement = await ctx.ui.editor('Refine the plan:', '');
298
- if (refinement?.trim()) {
299
- shortcut.sendUserMessage(refinement.trim());
300
- }
301
- }
302
- });
303
-
304
- // Restore state on session start/resume
305
- shortcut.on('session_start', async (_event, ctx) => {
306
- if (shortcut.getFlag('plan') === true) {
307
- planModeEnabled = true;
308
- }
309
-
310
- const entries = ctx.sessionManager.getEntries();
311
-
312
- // Restore persisted state
313
- const planModeEntry = entries
314
- .filter(
315
- (e: { type: string; customType?: string }) =>
316
- e.type === 'custom' && e.customType === 'plan-mode'
317
- )
318
- .pop() as
319
- | { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } }
320
- | undefined;
321
-
322
- if (planModeEntry?.data) {
323
- planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
324
- todoItems = planModeEntry.data.todos ?? todoItems;
325
- executionMode = planModeEntry.data.executing ?? executionMode;
326
- }
327
-
328
- // On resume: re-scan messages to rebuild completion state
329
- // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
330
- const isResume = planModeEntry !== undefined;
331
- if (isResume && executionMode && todoItems.length > 0) {
332
- // Find the index of the last plan-mode-execute entry (marks when current execution started)
333
- let executeIndex = -1;
334
- for (let i = entries.length - 1; i >= 0; i--) {
335
- const entry = entries[i] as { type: string; customType?: string };
336
- if (entry.customType === 'plan-mode-execute') {
337
- executeIndex = i;
338
- break;
339
- }
340
- }
341
-
342
- // Only scan messages after the execute marker
343
- const messages: AssistantMessage[] = [];
344
- for (let i = executeIndex + 1; i < entries.length; i++) {
345
- const entry = entries[i];
346
- if (
347
- entry.type === 'message' &&
348
- 'message' in entry &&
349
- isAssistantMessage(entry.message as AgentMessage)
350
- ) {
351
- messages.push(entry.message as AssistantMessage);
352
- }
353
- }
354
- const allText = messages.map(getTextContent).join('\n');
355
- markCompletedSteps(allText, todoItems);
356
- }
357
-
358
- if (planModeEnabled) {
359
- shortcut.setActiveTools(PLAN_MODE_TOOLS);
360
- }
361
- updateStatus(ctx);
362
- });
363
- }
1
+ /**
2
+ * Plan Mode Extension
3
+ *
4
+ * Read-only exploration mode for safe code analysis.
5
+ * When enabled, only read-only tools are available.
6
+ *
7
+ * Features:
8
+ * - /plan command or Ctrl+Alt+P to toggle
9
+ * - Bash restricted to allowlisted read-only commands
10
+ * - Extracts numbered plan steps from "Plan:" sections
11
+ * - [DONE:n] markers to complete steps during execution
12
+ * - Progress tracking widget during execution
13
+ */
14
+
15
+ import type {
16
+ AgentMessage,
17
+ AssistantMessage,
18
+ ExtensionAPI,
19
+ ExtensionContext,
20
+ TextContent
21
+ } from 'shortcutxl';
22
+ import { Key } from 'shortcutxl';
23
+ import { extractTodoItems, isSafeCommand, markCompletedSteps, type TodoItem } from './utils.js';
24
+
25
+ // Tools
26
+ const PLAN_MODE_TOOLS = ['read', 'bash', 'grep', 'find', 'ls', 'questionnaire'];
27
+ const NORMAL_MODE_TOOLS = ['read', 'bash', 'edit', 'write'];
28
+
29
+ // Type guard for assistant messages
30
+ function isAssistantMessage(m: AgentMessage): m is AssistantMessage {
31
+ return m.role === 'assistant' && Array.isArray(m.content);
32
+ }
33
+
34
+ // Extract text content from an assistant message
35
+ function getTextContent(message: AssistantMessage): string {
36
+ return message.content
37
+ .filter((block): block is TextContent => block.type === 'text')
38
+ .map((block) => block.text)
39
+ .join('\n');
40
+ }
41
+
42
+ export default function planModeExtension(shortcut: ExtensionAPI): void {
43
+ let planModeEnabled = false;
44
+ let executionMode = false;
45
+ let todoItems: TodoItem[] = [];
46
+
47
+ shortcut.registerFlag('plan', {
48
+ description: 'Start in plan mode (read-only exploration)',
49
+ type: 'boolean',
50
+ default: false
51
+ });
52
+
53
+ function updateStatus(ctx: ExtensionContext): void {
54
+ // Footer status
55
+ if (executionMode && todoItems.length > 0) {
56
+ const completed = todoItems.filter((t) => t.completed).length;
57
+ ctx.ui.setStatus(
58
+ 'plan-mode',
59
+ ctx.ui.theme.fg('accent', `📋 ${completed}/${todoItems.length}`)
60
+ );
61
+ } else if (planModeEnabled) {
62
+ ctx.ui.setStatus('plan-mode', ctx.ui.theme.fg('warning', '⏸ plan'));
63
+ } else {
64
+ ctx.ui.setStatus('plan-mode', undefined);
65
+ }
66
+
67
+ // Widget showing todo list
68
+ if (executionMode && todoItems.length > 0) {
69
+ const lines = todoItems.map((item) => {
70
+ if (item.completed) {
71
+ return (
72
+ ctx.ui.theme.fg('success', '☑ ') +
73
+ ctx.ui.theme.fg('muted', ctx.ui.theme.strikethrough(item.text))
74
+ );
75
+ }
76
+ return `${ctx.ui.theme.fg('muted', '☐ ')}${item.text}`;
77
+ });
78
+ ctx.ui.setWidget('plan-todos', lines);
79
+ } else {
80
+ ctx.ui.setWidget('plan-todos', undefined);
81
+ }
82
+ }
83
+
84
+ function togglePlanMode(ctx: ExtensionContext): void {
85
+ planModeEnabled = !planModeEnabled;
86
+ executionMode = false;
87
+ todoItems = [];
88
+
89
+ if (planModeEnabled) {
90
+ shortcut.setActiveTools(PLAN_MODE_TOOLS);
91
+ ctx.ui.notify(`Plan mode enabled. Tools: ${PLAN_MODE_TOOLS.join(', ')}`);
92
+ } else {
93
+ shortcut.setActiveTools(NORMAL_MODE_TOOLS);
94
+ ctx.ui.notify('Plan mode disabled. Full access restored.');
95
+ }
96
+ updateStatus(ctx);
97
+ }
98
+
99
+ function persistState(): void {
100
+ shortcut.appendEntry('plan-mode', {
101
+ enabled: planModeEnabled,
102
+ todos: todoItems,
103
+ executing: executionMode
104
+ });
105
+ }
106
+
107
+ shortcut.registerCommand('plan', {
108
+ description: 'Toggle plan mode (read-only exploration)',
109
+ handler: async (_args, ctx) => togglePlanMode(ctx)
110
+ });
111
+
112
+ shortcut.registerCommand('todos', {
113
+ description: 'Show current plan todo list',
114
+ handler: async (_args, ctx) => {
115
+ if (todoItems.length === 0) {
116
+ ctx.ui.notify('No todos. Create a plan first with /plan', 'info');
117
+ return;
118
+ }
119
+ const list = todoItems
120
+ .map((item, i) => `${i + 1}. ${item.completed ? '✓' : '○'} ${item.text}`)
121
+ .join('\n');
122
+ ctx.ui.notify(`Plan Progress:\n${list}`, 'info');
123
+ }
124
+ });
125
+
126
+ shortcut.registerShortcut(Key.ctrlAlt('p'), {
127
+ description: 'Toggle plan mode',
128
+ handler: async (ctx) => togglePlanMode(ctx)
129
+ });
130
+
131
+ // Block destructive bash commands in plan mode
132
+ shortcut.on('tool_call', async (event) => {
133
+ if (!planModeEnabled || event.toolName !== 'bash') return;
134
+
135
+ const command = event.input.command as string;
136
+ if (!isSafeCommand(command)) {
137
+ return {
138
+ block: true,
139
+ reason: `Plan mode: command blocked (not allowlisted). Use /plan to disable plan mode first.\nCommand: ${command}`
140
+ };
141
+ }
142
+ });
143
+
144
+ // Filter out stale plan mode context when not in plan mode
145
+ shortcut.on('context', async (event) => {
146
+ if (planModeEnabled) return;
147
+
148
+ return {
149
+ messages: event.messages.filter((m) => {
150
+ const msg = m as AgentMessage & { customType?: string };
151
+ if (msg.customType === 'plan-mode-context') return false;
152
+ if (msg.role !== 'user') return true;
153
+
154
+ const content = msg.content;
155
+ if (typeof content === 'string') {
156
+ return !content.includes('[PLAN MODE ACTIVE]');
157
+ }
158
+ if (Array.isArray(content)) {
159
+ return !content.some(
160
+ (c) => c.type === 'text' && (c as TextContent).text?.includes('[PLAN MODE ACTIVE]')
161
+ );
162
+ }
163
+ return true;
164
+ })
165
+ };
166
+ });
167
+
168
+ // Inject plan/execution context before agent starts
169
+ shortcut.on('before_agent_start', async () => {
170
+ if (planModeEnabled) {
171
+ return {
172
+ message: {
173
+ customType: 'plan-mode-context',
174
+ content: `[PLAN MODE ACTIVE]
175
+ You are in plan mode - a read-only exploration mode for safe code analysis.
176
+
177
+ Restrictions:
178
+ - You can only use: read, bash, grep, find, ls, questionnaire
179
+ - You CANNOT use: edit, write (file modifications are disabled)
180
+ - Bash is restricted to an allowlist of read-only commands
181
+
182
+ Ask clarifying questions using the questionnaire tool.
183
+ Use brave-search skill via bash for web research.
184
+
185
+ Create a detailed numbered plan under a "Plan:" header:
186
+
187
+ Plan:
188
+ 1. First step description
189
+ 2. Second step description
190
+ ...
191
+
192
+ Do NOT attempt to make changes - just describe what you would do.`,
193
+ display: false
194
+ }
195
+ };
196
+ }
197
+
198
+ if (executionMode && todoItems.length > 0) {
199
+ const remaining = todoItems.filter((t) => !t.completed);
200
+ const todoList = remaining.map((t) => `${t.step}. ${t.text}`).join('\n');
201
+ return {
202
+ message: {
203
+ customType: 'plan-execution-context',
204
+ content: `[EXECUTING PLAN - Full tool access enabled]
205
+
206
+ Remaining steps:
207
+ ${todoList}
208
+
209
+ Execute each step in order.
210
+ After completing a step, include a [DONE:n] tag in your response.`,
211
+ display: false
212
+ }
213
+ };
214
+ }
215
+ });
216
+
217
+ // Track progress after each turn
218
+ shortcut.on('turn_end', async (event, ctx) => {
219
+ if (!executionMode || todoItems.length === 0) return;
220
+ if (!isAssistantMessage(event.message)) return;
221
+
222
+ const text = getTextContent(event.message);
223
+ if (markCompletedSteps(text, todoItems) > 0) {
224
+ updateStatus(ctx);
225
+ }
226
+ persistState();
227
+ });
228
+
229
+ // Handle plan completion and plan mode UI
230
+ shortcut.on('agent_end', async (event, ctx) => {
231
+ // Check if execution is complete
232
+ if (executionMode && todoItems.length > 0) {
233
+ if (todoItems.every((t) => t.completed)) {
234
+ const completedList = todoItems.map((t) => `~~${t.text}~~`).join('\n');
235
+ shortcut.sendMessage(
236
+ {
237
+ customType: 'plan-complete',
238
+ content: `**Plan Complete!** ✓\n\n${completedList}`,
239
+ display: true
240
+ },
241
+ { triggerTurn: false }
242
+ );
243
+ executionMode = false;
244
+ todoItems = [];
245
+ shortcut.setActiveTools(NORMAL_MODE_TOOLS);
246
+ updateStatus(ctx);
247
+ persistState(); // Save cleared state so resume doesn't restore old execution mode
248
+ }
249
+ return;
250
+ }
251
+
252
+ if (!planModeEnabled || !ctx.hasUI) return;
253
+
254
+ // Extract todos from last assistant message
255
+ const lastAssistant = [...event.messages].reverse().find(isAssistantMessage);
256
+ if (lastAssistant) {
257
+ const extracted = extractTodoItems(getTextContent(lastAssistant));
258
+ if (extracted.length > 0) {
259
+ todoItems = extracted;
260
+ }
261
+ }
262
+
263
+ // Show plan steps and prompt for next action
264
+ if (todoItems.length > 0) {
265
+ const todoListText = todoItems.map((t, i) => `${i + 1}. ☐ ${t.text}`).join('\n');
266
+ shortcut.sendMessage(
267
+ {
268
+ customType: 'plan-todo-list',
269
+ content: `**Plan Steps (${todoItems.length}):**\n\n${todoListText}`,
270
+ display: true
271
+ },
272
+ { triggerTurn: false }
273
+ );
274
+ }
275
+
276
+ const choice = await ctx.ui.select('Plan mode - what next?', [
277
+ todoItems.length > 0 ? 'Execute the plan (track progress)' : 'Execute the plan',
278
+ 'Stay in plan mode',
279
+ 'Refine the plan'
280
+ ]);
281
+
282
+ if (choice?.startsWith('Execute')) {
283
+ planModeEnabled = false;
284
+ executionMode = todoItems.length > 0;
285
+ shortcut.setActiveTools(NORMAL_MODE_TOOLS);
286
+ updateStatus(ctx);
287
+
288
+ const execMessage =
289
+ todoItems.length > 0
290
+ ? `Execute the plan. Start with: ${todoItems[0].text}`
291
+ : 'Execute the plan you just created.';
292
+ shortcut.sendMessage(
293
+ { customType: 'plan-mode-execute', content: execMessage, display: true },
294
+ { triggerTurn: true }
295
+ );
296
+ } else if (choice === 'Refine the plan') {
297
+ const refinement = await ctx.ui.editor('Refine the plan:', '');
298
+ if (refinement?.trim()) {
299
+ shortcut.sendUserMessage(refinement.trim());
300
+ }
301
+ }
302
+ });
303
+
304
+ // Restore state on session start/resume
305
+ shortcut.on('session_start', async (_event, ctx) => {
306
+ if (shortcut.getFlag('plan') === true) {
307
+ planModeEnabled = true;
308
+ }
309
+
310
+ const entries = ctx.sessionManager.getEntries();
311
+
312
+ // Restore persisted state
313
+ const planModeEntry = entries
314
+ .filter(
315
+ (e: { type: string; customType?: string }) =>
316
+ e.type === 'custom' && e.customType === 'plan-mode'
317
+ )
318
+ .pop() as
319
+ | { data?: { enabled: boolean; todos?: TodoItem[]; executing?: boolean } }
320
+ | undefined;
321
+
322
+ if (planModeEntry?.data) {
323
+ planModeEnabled = planModeEntry.data.enabled ?? planModeEnabled;
324
+ todoItems = planModeEntry.data.todos ?? todoItems;
325
+ executionMode = planModeEntry.data.executing ?? executionMode;
326
+ }
327
+
328
+ // On resume: re-scan messages to rebuild completion state
329
+ // Only scan messages AFTER the last "plan-mode-execute" to avoid picking up [DONE:n] from previous plans
330
+ const isResume = planModeEntry !== undefined;
331
+ if (isResume && executionMode && todoItems.length > 0) {
332
+ // Find the index of the last plan-mode-execute entry (marks when current execution started)
333
+ let executeIndex = -1;
334
+ for (let i = entries.length - 1; i >= 0; i--) {
335
+ const entry = entries[i] as { type: string; customType?: string };
336
+ if (entry.customType === 'plan-mode-execute') {
337
+ executeIndex = i;
338
+ break;
339
+ }
340
+ }
341
+
342
+ // Only scan messages after the execute marker
343
+ const messages: AssistantMessage[] = [];
344
+ for (let i = executeIndex + 1; i < entries.length; i++) {
345
+ const entry = entries[i];
346
+ if (
347
+ entry.type === 'message' &&
348
+ 'message' in entry &&
349
+ isAssistantMessage(entry.message as AgentMessage)
350
+ ) {
351
+ messages.push(entry.message as AssistantMessage);
352
+ }
353
+ }
354
+ const allText = messages.map(getTextContent).join('\n');
355
+ markCompletedSteps(allText, todoItems);
356
+ }
357
+
358
+ if (planModeEnabled) {
359
+ shortcut.setActiveTools(PLAN_MODE_TOOLS);
360
+ }
361
+ updateStatus(ctx);
362
+ });
363
+ }