@shawnstack/quickforge 1.7.11 → 1.8.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.
Files changed (122) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/AgentProfilesPage-lKGIE5Dj.js +1 -0
  3. package/dist/assets/ChatPanelHost-CWUYIkXG.js +11 -0
  4. package/dist/assets/CloudAccountSettingsPage-BhXH0ups.js +1 -0
  5. package/dist/assets/{PluginsPage-mLvwvTmL.js → PluginsPage-DHWF13mN.js} +1 -1
  6. package/dist/assets/ScheduledTasksPage-DLAg-i8c.js +2 -0
  7. package/dist/assets/{SettingsWorkspacePage-0OINfEqK.js → SettingsWorkspacePage-BhUzL5oi.js} +313 -313
  8. package/dist/assets/ShareLinksSettingsPage-DogGa3lH.js +1 -0
  9. package/dist/assets/SharedConversationPage-7QpeaG_P.js +1 -0
  10. package/dist/assets/TerminalDock-CWSKcuiU.js +2 -0
  11. package/dist/assets/WorkspaceInspector-CEfyYqWy.js +36 -0
  12. package/dist/assets/{abnfDiagram-VRR7QNED-CMhVoYUh.js → abnfDiagram-VRR7QNED-DGklo3PO.js} +1 -1
  13. package/dist/assets/{architectureDiagram-ZJ3FMSHR-C52qL6Hg.js → architectureDiagram-ZJ3FMSHR-CioWLVTp.js} +1 -1
  14. package/dist/assets/{blockDiagram-677ZJIJ3-CM3XbLol.js → blockDiagram-677ZJIJ3-BJWBq5e9.js} +1 -1
  15. package/dist/assets/{c4Diagram-LMCZKHZV-D7Bh1ui1.js → c4Diagram-LMCZKHZV-BacRvqFK.js} +1 -1
  16. package/dist/assets/channel-CeXY-wZo.js +1 -0
  17. package/dist/assets/{chunk-32BRIVSS-xgrtbT8e.js → chunk-32BRIVSS-B53BNE2C.js} +1 -1
  18. package/dist/assets/{chunk-52WLFC77-CDbaHw80.js → chunk-52WLFC77-CYq4hT4f.js} +1 -1
  19. package/dist/assets/{chunk-C7G6YPKG-ZMLUvfLy.js → chunk-C7G6YPKG-KFaGpP_Z.js} +1 -1
  20. package/dist/assets/{chunk-EX3LRPZG-BcQhaqt7.js → chunk-EX3LRPZG-DqVd_ihG.js} +1 -1
  21. package/dist/assets/{chunk-FWX5IMBZ-DrnrdrSu.js → chunk-FWX5IMBZ-BxRjZkOP.js} +2 -2
  22. package/dist/assets/{chunk-HOUHSVGY-tdc-Z9eW.js → chunk-HOUHSVGY-C9QO8omv.js} +1 -1
  23. package/dist/assets/{chunk-ICXQ74PX-Cd7igVGt.js → chunk-ICXQ74PX-ClbGPgLV.js} +1 -1
  24. package/dist/assets/{chunk-MOJQB5TN-CLTjV5ov.js → chunk-MOJQB5TN-Ba4wlF1J.js} +1 -1
  25. package/dist/assets/{chunk-OGEWGWER-TgvkXkFX.js → chunk-OGEWGWER-DczhQmcx.js} +1 -1
  26. package/dist/assets/{chunk-PUDLZKDR-C5GMVYup.js → chunk-PUDLZKDR-CPWpcKUF.js} +1 -1
  27. package/dist/assets/{chunk-Q4XR5HBZ-fo5-HsqY.js → chunk-Q4XR5HBZ-CiF6T--X.js} +1 -1
  28. package/dist/assets/{chunk-V7JOEXUC-CEDazV6w.js → chunk-V7JOEXUC-Dxv0zwmw.js} +1 -1
  29. package/dist/assets/{chunk-VAUOI2AC-CWYjg8zE.js → chunk-VAUOI2AC-D3J5bw1P.js} +1 -1
  30. package/dist/assets/{chunk-VR4S4FIN-C_-eEj-9.js → chunk-VR4S4FIN-BXQSwwII.js} +1 -1
  31. package/dist/assets/{chunk-WYO6CB5R-NF-MsJJR.js → chunk-WYO6CB5R-C0azl-fk.js} +2 -2
  32. package/dist/assets/{chunk-ZGVPDNZ5-CRrAYi4g.js → chunk-ZGVPDNZ5-D_8F4Akx.js} +1 -1
  33. package/dist/assets/classDiagram-OUVF2IWQ-B8M6sf49.js +1 -0
  34. package/dist/assets/classDiagram-v2-EOCWNBFH-BY7fzBDH.js +1 -0
  35. package/dist/assets/{cynefinDiagram-TSTJHNR4-D4sZ7LYI.js → cynefinDiagram-TSTJHNR4-BFlsWz0B.js} +1 -1
  36. package/dist/assets/{dagre-VKFMJZFB-BlsnSh-Q.js → dagre-VKFMJZFB-DXhpjlEs.js} +1 -1
  37. package/dist/assets/{diagram-FQU43EPY-DW1yQiRO.js → diagram-FQU43EPY-DRVVOD9k.js} +1 -1
  38. package/dist/assets/{diagram-G47NLZAW-BTUAsFjb.js → diagram-G47NLZAW-BV9Gw1tL.js} +1 -1
  39. package/dist/assets/{diagram-NH7WQ7WH-BcaomqSd.js → diagram-NH7WQ7WH-CrUkXfRY.js} +1 -1
  40. package/dist/assets/{diagram-OA4YK3LP-DDLX7Xwr.js → diagram-OA4YK3LP-CuILjn7v.js} +1 -1
  41. package/dist/assets/{diagram-WEI45ONY-CIave0An.js → diagram-WEI45ONY-nlNN3tWB.js} +1 -1
  42. package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js → ebnfDiagram-CCIWWBDH-BuDgZt9n.js} +1 -1
  43. package/dist/assets/{erDiagram-Q63AITRT-D3EgiVVH.js → erDiagram-Q63AITRT-MEwxjmtP.js} +1 -1
  44. package/dist/assets/flowDiagram-23GEKE2U-BiUP-Scq.js +1 -0
  45. package/dist/assets/{ganttDiagram-NO4QXBWP-B0oyDIqK.js → ganttDiagram-NO4QXBWP-DNw5qghE.js} +1 -1
  46. package/dist/assets/{gitGraphDiagram-IHSO6WYX-uQtr6IqK.js → gitGraphDiagram-IHSO6WYX-CaTcakAt.js} +1 -1
  47. package/dist/assets/index-CX36XjVA.css +3 -0
  48. package/dist/assets/index-CgVTwN15.js +66 -0
  49. package/dist/assets/{infoDiagram-FWYZ7A6U-B_GzlLIM.js → infoDiagram-FWYZ7A6U-DwfyP7dU.js} +1 -1
  50. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKQIsSdU.js → ishikawaDiagram-FXEZZL3T-CmrzbOCV.js} +1 -1
  51. package/dist/assets/{journeyDiagram-5HDEW3XC-DrrvzZL1.js → journeyDiagram-5HDEW3XC-CYqe3sgr.js} +1 -1
  52. package/dist/assets/{kanban-definition-HUTT4EX6-CJQRTdXz.js → kanban-definition-HUTT4EX6-BMDO_mv0.js} +1 -1
  53. package/dist/assets/{line-B4DxX7tC.js → line-SbxJR8ZT.js} +1 -1
  54. package/dist/assets/lit-vendor-CinchGmI.js +2 -0
  55. package/dist/assets/local-tools-CJVNO1aW.js +388 -0
  56. package/dist/assets/{mcp-servers-dialog-B2lALtTE.js → mcp-servers-dialog-xf3UR49J.js} +2 -2
  57. package/dist/assets/{mermaid.core-DWpShXCs.js → mermaid.core-CwXTzYjs.js} +3 -3
  58. package/dist/assets/{mindmap-definition-LN4V7U3C-CEVwZD78.js → mindmap-definition-LN4V7U3C-CLyJ78mO.js} +1 -1
  59. package/dist/assets/{pegDiagram-2B236MQR-CBLkBdnQ.js → pegDiagram-2B236MQR-4qFznrSw.js} +1 -1
  60. package/dist/assets/{pi-web-ui-DV5jKjSg.js → pi-web-ui-BNuvqKDT.js} +289 -289
  61. package/dist/assets/{pieDiagram-ENE6RG2P-8tCnwtWS.js → pieDiagram-ENE6RG2P-141sGxVe.js} +1 -1
  62. package/dist/assets/{quadrantDiagram-ABIIQ3AL-CWd28cHF.js → quadrantDiagram-ABIIQ3AL-SUI44pAX.js} +1 -1
  63. package/dist/assets/{railroadDiagram-RFXS5EU6-CI4ouOBs.js → railroadDiagram-RFXS5EU6-1oiZuYBB.js} +1 -1
  64. package/dist/assets/{requirementDiagram-TGXJPOKE-DtFz7sw4.js → requirementDiagram-TGXJPOKE-CfVishLi.js} +1 -1
  65. package/dist/assets/{sankeyDiagram-HTMAVEWB--VeOwwk9.js → sankeyDiagram-HTMAVEWB-DVu7J_Z9.js} +1 -1
  66. package/dist/assets/{sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js → sequenceDiagram-DBY2YBRQ-D-Ip2yiS.js} +1 -1
  67. package/dist/assets/{skills-dialog-DYp35JM3.js → skills-dialog-Bbqtuy2h.js} +3 -3
  68. package/dist/assets/{stateDiagram-2N3HPSRC-etXnJ8X8.js → stateDiagram-2N3HPSRC-Cg0hWwrp.js} +1 -1
  69. package/dist/assets/stateDiagram-v2-6OUMAXLB-DhmzNQd9.js +1 -0
  70. package/dist/assets/{swimlanes-5IMT3BWC-pabIKAmn.js → swimlanes-5IMT3BWC-Co5D-cGU.js} +1 -1
  71. package/dist/assets/swimlanesDiagram-G3AALYLV-BEvomQNa.js +8 -0
  72. package/dist/assets/{timeline-definition-FHXFAJF6-C55MKm3L.js → timeline-definition-FHXFAJF6-CBk4c_Ua.js} +1 -1
  73. package/dist/assets/{vennDiagram-L72KCM5P-CgNf1Q9W.js → vennDiagram-L72KCM5P-BII7wdRx.js} +1 -1
  74. package/dist/assets/{wardleyDiagram-EHGQE667-Drt5ytec.js → wardleyDiagram-EHGQE667-CkyJJIXP.js} +1 -1
  75. package/dist/assets/{xychartDiagram-FW5EYKEG-hhoEo_Ar.js → xychartDiagram-FW5EYKEG-B1A66Ks0.js} +1 -1
  76. package/dist/index.html +6 -6
  77. package/dist/licenses/material-icon-theme.txt +8 -8
  78. package/package.json +1 -1
  79. package/server/agent-manager.mjs +305 -49
  80. package/server/approval-store.mjs +1 -0
  81. package/server/ask-store.mjs +78 -0
  82. package/server/context-references.mjs +139 -0
  83. package/server/custom-commands.mjs +43 -0
  84. package/server/index.mjs +1 -1
  85. package/server/project-config.mjs +17 -5
  86. package/server/routes/agent-profiles.mjs +16 -1
  87. package/server/routes/agent.mjs +10 -1
  88. package/server/routes/shared-conversation.mjs +13 -1
  89. package/server/routes/skills.mjs +22 -0
  90. package/server/routes/workspace.mjs +166 -2
  91. package/server/selected-capabilities.mjs +76 -0
  92. package/server/session-state-import.mjs +20 -3
  93. package/server/skills.mjs +1 -1
  94. package/server/system-prompt.mjs +1 -3
  95. package/server/tools/definitions.mjs +18 -10
  96. package/server/tools/index.mjs +14 -2
  97. package/server/utils/workspace.mjs +13 -4
  98. package/skills/skill-creator/SKILL.md +485 -485
  99. package/skills/skill-creator/agents/analyzer.md +274 -274
  100. package/skills/skill-creator/agents/comparator.md +202 -202
  101. package/skills/skill-creator/agents/grader.md +223 -223
  102. package/skills/skill-creator/assets/eval_review.html +146 -146
  103. package/skills/skill-creator/eval-viewer/viewer.html +1325 -1325
  104. package/skills/skill-creator/references/schemas.md +430 -430
  105. package/dist/assets/AgentProfilesPage-Cvb1KTz5.js +0 -1
  106. package/dist/assets/ChatPanelHost-C7zV-xE_.js +0 -48
  107. package/dist/assets/CloudAccountSettingsPage-mAs_nbvw.js +0 -1
  108. package/dist/assets/ScheduledTasksPage-CuUW_9lq.js +0 -2
  109. package/dist/assets/ShareLinksSettingsPage-DqeyJYoD.js +0 -1
  110. package/dist/assets/SharedConversationPage-C7CDEoX9.js +0 -1
  111. package/dist/assets/TerminalDock-CZXcuMpq.js +0 -2
  112. package/dist/assets/WorkspaceInspector-CNSBOEvP.js +0 -36
  113. package/dist/assets/channel-Duu1CuH9.js +0 -1
  114. package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +0 -1
  115. package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +0 -1
  116. package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +0 -1
  117. package/dist/assets/index-BgMvQW1b.js +0 -66
  118. package/dist/assets/index-CB1g2D0f.css +0 -3
  119. package/dist/assets/lit-vendor-DvaQX_UQ.js +0 -2
  120. package/dist/assets/local-tools-Dopjg9xq.js +0 -270
  121. package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +0 -1
  122. package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +0 -8
@@ -0,0 +1,76 @@
1
+ export const MAX_SELECTED_CAPABILITIES = 4
2
+ export const SELECTED_CAPABILITY_PLUGIN_NAME_MAX_LENGTH = 120
3
+ export const SELECTED_CAPABILITY_NAME_MAX_LENGTH = 120
4
+ export const SELECTED_CAPABILITY_LABEL_MAX_LENGTH = 160
5
+ export const SELECTED_CAPABILITY_DESCRIPTION_MAX_LENGTH = 400
6
+ export const SELECTED_CAPABILITIES_DETAILS_KEY = 'selectedCapabilities'
7
+
8
+ const SELECTED_CAPABILITY_TYPES = new Set(['plugin', 'skill', 'tool', 'command'])
9
+
10
+ function normalizedText(value, maxLength) {
11
+ if (typeof value !== 'string') return undefined
12
+ const normalized = value.trim().slice(0, maxLength)
13
+ return normalized || undefined
14
+ }
15
+
16
+ export function selectedCapabilityKey(capability) {
17
+ return `${capability.type}:${capability.pluginName}:${capability.name}`
18
+ }
19
+
20
+ export function normalizeSelectedCapabilities(value) {
21
+ if (!Array.isArray(value)) return []
22
+ const result = []
23
+ const seen = new Set()
24
+ for (const item of value) {
25
+ if (!item || typeof item !== 'object' || Array.isArray(item) || !SELECTED_CAPABILITY_TYPES.has(item.type)) continue
26
+ const pluginName = normalizedText(item.pluginName, SELECTED_CAPABILITY_PLUGIN_NAME_MAX_LENGTH)
27
+ const name = normalizedText(item.name, SELECTED_CAPABILITY_NAME_MAX_LENGTH)
28
+ const label = normalizedText(item.label, SELECTED_CAPABILITY_LABEL_MAX_LENGTH)
29
+ if (!pluginName || !name || !label) continue
30
+ const capability = { type: item.type, pluginName, name, label }
31
+ const description = normalizedText(item.description, SELECTED_CAPABILITY_DESCRIPTION_MAX_LENGTH)
32
+ if (description) capability.description = description
33
+ const key = selectedCapabilityKey(capability)
34
+ if (seen.has(key)) continue
35
+ seen.add(key)
36
+ result.push(capability)
37
+ if (result.length >= MAX_SELECTED_CAPABILITIES) break
38
+ }
39
+ return result
40
+ }
41
+
42
+ export function selectedCapabilitySnapshots(value) {
43
+ return normalizeSelectedCapabilities(value).map(({ type, pluginName, name, label }) => ({ type, pluginName, name, label }))
44
+ }
45
+
46
+ export function selectedCapabilitiesFromMessage(message) {
47
+ const details = message?.details
48
+ if (!details || typeof details !== 'object' || Array.isArray(details)) return []
49
+ return selectedCapabilitySnapshots(details[SELECTED_CAPABILITIES_DETAILS_KEY])
50
+ }
51
+
52
+ export function withCanonicalSelectedCapabilities(message, capabilities) {
53
+ if (!message || typeof message !== 'object') return message
54
+ const snapshots = selectedCapabilitySnapshots(capabilities)
55
+ const details = message.details && typeof message.details === 'object' && !Array.isArray(message.details)
56
+ ? message.details
57
+ : {}
58
+ const nextDetails = { ...details }
59
+ if (snapshots.length > 0) nextDetails[SELECTED_CAPABILITIES_DETAILS_KEY] = snapshots
60
+ else delete nextDetails[SELECTED_CAPABILITIES_DETAILS_KEY]
61
+ const next = { ...message }
62
+ if (Object.keys(nextDetails).length > 0) next.details = nextDetails
63
+ else delete next.details
64
+ return next
65
+ }
66
+
67
+ export function selectedCapabilityPrompt(capabilities) {
68
+ const normalized = normalizeSelectedCapabilities(capabilities)
69
+ if (normalized.length === 0) return null
70
+ const lines = normalized.map((capability) => {
71
+ const toolHint = capability.type === 'tool' ? ` Tool name: plugin__${capability.pluginName}__${capability.name}.` : ''
72
+ const description = capability.description ? ` Description: ${capability.description}` : ''
73
+ return `- ${capability.label} (${capability.type}, plugin: ${capability.pluginName}, name: ${capability.name}).${toolHint}${description}`
74
+ }).join('\n')
75
+ return `The user selected the following QuickForge plugin capability mentions for this turn. Treat them as an explicit preference for routing and context. Use the selected capability when relevant, but do not force it if it is unrelated to the actual request.\n\n${lines}`
76
+ }
@@ -98,7 +98,12 @@ function normalizeSessionEntry(bucket, sessionId, rawState, rawMetadata, diagnos
98
98
  // Resilience: a failing entry (unreadable file, validation mismatch,
99
99
  // cross-bucket duplicate) never aborts the run — it is counted in `skipped`
100
100
  // and described in `diagnostics` so the operator can inspect and fix the
101
- // source tree, then simply re-run.
101
+ // source tree, then simply re-run. The same holds one level up: an unreadable
102
+ // or malformed sessions-metadata.json (the physical adapter only swallows
103
+ // ENOENT — corrupt JSON, EACCES/EBUSY rethrow) degrades that bucket to empty
104
+ // metadata instead of failing the import, and with it startup: body files
105
+ // still import through deriveMetadata, only metadata-only entries are lost
106
+ // (they carry no messages and are dropped by design anyway).
102
107
  export async function importSessionStateFromJson({ storage = null, logger = defaultLogger } = {}) {
103
108
  const storageModule = storage ?? await import('./storage.mjs')
104
109
  const fsAdapter = storageModule.createPhysicalSessionStateFsAdapter()
@@ -109,8 +114,20 @@ export async function importSessionStateFromJson({ storage = null, logger = defa
109
114
 
110
115
  for await (const rawBucket of fsAdapter.listBuckets()) {
111
116
  const bucket = normalizeBucket(rawBucket)
112
- const metadata = await fsAdapter.readMetadataBucket(rawBucket)
113
- if (!isPlainObject(metadata)) throw new TypeError('Session bucket stores must be objects')
117
+ let metadata = {}
118
+ try {
119
+ const rawMetadata = await fsAdapter.readMetadataBucket(rawBucket)
120
+ if (!isPlainObject(rawMetadata)) throw new TypeError('Session bucket stores must be objects')
121
+ metadata = rawMetadata
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error)
124
+ diagnostics.push({ kind: 'metadata-bucket-error', scope: bucket.scope, projectId: bucket.projectId, message })
125
+ logger.warn('Session metadata bucket unreadable; importing with body-derived metadata', {
126
+ scope: bucket.scope,
127
+ projectId: bucket.projectId,
128
+ message,
129
+ })
130
+ }
114
131
  const fileIds = new Set()
115
132
  for await (const sessionId of fsAdapter.listSessionFiles(rawBucket)) fileIds.add(sessionId)
116
133
  const ids = [...new Set([...fileIds, ...Object.keys(metadata)])].sort((left, right) => left.localeCompare(right))
package/server/skills.mjs CHANGED
@@ -392,7 +392,7 @@ function searchDirsForList(value) {
392
392
  return value.length === 1 ? value[0] : value.slice()
393
393
  }
394
394
 
395
- function summarizeSkills(skills) {
395
+ export function summarizeSkills(skills) {
396
396
  return skills.map(({ instructions: _instructions, rootDir: _rootDir, location: _location, ...summary }) => summary)
397
397
  }
398
398
 
@@ -2,8 +2,7 @@ export const BASE_SYSTEM_PROMPT = `You are a pragmatic coding assistant.
2
2
 
3
3
  For project tasks:
4
4
  - Do not assume requirements. If ambiguous, state assumptions or ask.
5
- - Prefer the simplest solution that satisfies the request.
6
- - Make surgical changes only. Do not refactor unrelated code.
5
+ - Do not refactor unrelated code.
7
6
  - Match existing style.
8
7
  - When content has room for visual explanation, first consider whether an SVG diagram can improve understanding.
9
8
  - For multi-step work, use a brief plan.
@@ -11,7 +10,6 @@ For project tasks:
11
10
  - Before taking action, confirm with the user.
12
11
  - Unless the change is trivial and localized to an already-known file, use Explore first for read-only repository research before implementation decisions; prefer Explore for file discovery, source location, broad searches, call-chain lookup, pattern lookup, impact analysis, and locating related tests, docs, wiki pages, or build scripts.
13
12
  - For complex multi-step work, use General only for bounded assistance; the parent assistant remains responsible for final decisions, minimal edits, and verification.
14
- - Make minimal, focused changes.
15
13
  - Prefer dedicated workspace tools for reading, editing, and searching files.
16
14
  - If dedicated tools are unavailable or insufficient, use the shell/command tool.
17
15
  - Use Python through the shell for reliable scripting, data processing, or file transformations.
@@ -54,8 +54,26 @@ export const globalMemoryTool = {
54
54
  executionMode: 'sequential',
55
55
  }
56
56
 
57
+ export const askUserTool = {
58
+ name: 'ask_user',
59
+ label: 'Ask user',
60
+ description: 'Ask the user one or more questions (up to 4) and wait for their answers before continuing. Use this when you need the user to choose between approaches, confirm a direction, or supply information you cannot infer. Questions should be self-contained; each question offers 2-4 options and the user may answer with free-form text instead. All questions are answered together, then the run resumes.',
61
+ parameters: Type.Object({
62
+ questions: Type.Array(Type.Object({
63
+ question: Type.String({ description: 'Complete, self-contained question text.' }),
64
+ options: Type.Optional(Type.Array(Type.Object({
65
+ label: Type.String({ description: 'Concise option label.' }),
66
+ description: Type.Optional(Type.String({ description: 'Optional one-line explanation of the trade-off.' })),
67
+ }), { maxItems: 4, description: '2-4 mutually exclusive options.' })),
68
+ multiSelect: Type.Optional(Type.Boolean({ description: 'Allow selecting multiple options.', default: false })),
69
+ allowCustom: Type.Optional(Type.Boolean({ description: 'Allow a free-form answer instead of the options.', default: true })),
70
+ }), { minItems: 1, maxItems: 4, description: 'Questions asked together in one card; the user answers all of them in one pass.' }),
71
+ }),
72
+ }
73
+
57
74
  export const workspaceTools = [
58
75
  subagentTool,
76
+ askUserTool,
59
77
  {
60
78
  name: 'read_file',
61
79
  label: 'Read file',
@@ -116,16 +134,6 @@ export const workspaceTools = [
116
134
  }),
117
135
  executionMode: 'sequential',
118
136
  },
119
- {
120
- name: 'generate_image',
121
- label: 'Generate image',
122
- description: 'Generate images with the configured OpenRouter provider and save them as assets owned by the current conversation. Use this when the user explicitly asks for a generated bitmap image. The operation may incur provider charges.',
123
- parameters: Type.Object({
124
- prompt: Type.String({ description: 'Detailed image-generation prompt.' }),
125
- model: Type.Optional(Type.String({ description: 'Optional OpenRouter image model ID. Defaults to google/gemini-2.5-flash-image.' })),
126
- }),
127
- executionMode: 'sequential',
128
- },
129
137
  {
130
138
  name: 'present_files',
131
139
  label: 'Present files',
@@ -486,7 +486,7 @@ export async function toolGrepFiles(params, context, runtime = {}) {
486
486
  }
487
487
 
488
488
  // --- write_file ---
489
- export async function toolWriteFile(params, context) {
489
+ export async function toolWriteFile(params, context, runtime = {}) {
490
490
  const file = resolveWorkspacePath(params?.path, context)
491
491
  await assertSafeWorkspacePath(file, context, { forWrite: true })
492
492
 
@@ -502,6 +502,12 @@ export async function toolWriteFile(params, context) {
502
502
  }
503
503
  const diff = createTextDiff(oldText, content, relativePath, { oldExists: existed })
504
504
 
505
+ // 写盘前先发一次 partial update,让前端在 running 阶段就能滚动显示 ±行数。
506
+ runtime.onUpdate?.({
507
+ content: [{ type: 'text', text: `${existed ? 'Writing' : 'Creating'} ${relativePath} (+${diff.addedLines} -${diff.removedLines})` }],
508
+ details: { running: true, path: relativePath, project: context?.project, diff: { addedLines: diff.addedLines, removedLines: diff.removedLines } },
509
+ })
510
+
505
511
  await fs.mkdir(path.dirname(file), { recursive: true })
506
512
  await fs.writeFile(file, content, 'utf8')
507
513
 
@@ -536,7 +542,7 @@ function convertToLineEnding(text, ending) {
536
542
  return ending === '\r\n' ? normalized.replaceAll('\n', '\r\n') : normalized
537
543
  }
538
544
 
539
- export async function toolEditFile(params, context) {
545
+ export async function toolEditFile(params, context, runtime = {}) {
540
546
  const file = resolveWorkspacePath(params?.path, context)
541
547
  await assertSafeWorkspacePath(file, context)
542
548
 
@@ -560,6 +566,12 @@ export async function toolEditFile(params, context) {
560
566
  const relativePath = toWorkspaceRelative(file, context)
561
567
  const diff = createTextDiff(text, nextText, relativePath)
562
568
 
569
+ // 写盘前先发一次 partial update,让前端在 running 阶段就能滚动显示 ±行数。
570
+ runtime.onUpdate?.({
571
+ content: [{ type: 'text', text: `Editing ${relativePath} (+${diff.addedLines} -${diff.removedLines})` }],
572
+ details: { running: true, path: relativePath, project: context?.project, diff: { addedLines: diff.addedLines, removedLines: diff.removedLines } },
573
+ })
574
+
563
575
  await fs.writeFile(file, nextText, 'utf8')
564
576
 
565
577
  return {
@@ -41,7 +41,7 @@ export function toWorkspaceRelative(fullPath, context) {
41
41
 
42
42
  export function isSensitiveWorkspacePath(fullPath, context) {
43
43
  const relative = toWorkspaceRelative(fullPath, context)
44
- const parts = relative.split('/')
44
+ const parts = relative.split('/').map((part) => part.toLocaleLowerCase())
45
45
  const name = parts.at(-1) || ''
46
46
  return (
47
47
  parts.includes('.git') ||
@@ -61,6 +61,13 @@ export function isSensitiveWorkspacePath(fullPath, context) {
61
61
  )
62
62
  }
63
63
 
64
+ function sensitivePathError(fullPath, context) {
65
+ return Object.assign(new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`), {
66
+ statusCode: 403,
67
+ errorCode: 'WORKSPACE_SENSITIVE_PATH',
68
+ })
69
+ }
70
+
64
71
  async function realpathNearestExistingParent(inputPath) {
65
72
  let current = path.resolve(inputPath)
66
73
  while (true) {
@@ -77,9 +84,7 @@ async function realpathNearestExistingParent(inputPath) {
77
84
 
78
85
  async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal, options = {}) {
79
86
  if (!options.allowSensitive && isSensitiveWorkspacePath(fullPath, context)) {
80
- const error = new Error(`Access to sensitive path is blocked: ${toWorkspaceRelative(fullPath, context)}`)
81
- error.statusCode = 403
82
- throw error
87
+ throw sensitivePathError(fullPath, context)
83
88
  }
84
89
 
85
90
  let targetReal
@@ -98,8 +103,12 @@ async function assertSafeWorkspacePathWithRoot(fullPath, context, workspaceReal,
98
103
  if (!isInside(workspaceReal, targetReal)) {
99
104
  const error = new Error(`Path resolves outside the selected project: ${toWorkspaceRelative(fullPath, context)}`)
100
105
  error.statusCode = 403
106
+ error.errorCode = 'WORKSPACE_PATH_ESCAPE'
101
107
  throw error
102
108
  }
109
+ if (!options.allowSensitive && isSensitiveWorkspacePath(targetReal, { workspaceRoot: workspaceReal })) {
110
+ throw sensitivePathError(fullPath, context)
111
+ }
103
112
  }
104
113
 
105
114
  export async function createWorkspacePathValidator(context) {