@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.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/CHANGELOG.md +54 -0
- package/README.md +1 -1
- package/dist/cli.js +4659 -4261
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +316 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +118 -755
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +5 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/usage-cli.ts +10 -1
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +209 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +157 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +210 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1892 -12226
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
package/src/tools/write.ts
CHANGED
|
@@ -49,7 +49,14 @@ import {
|
|
|
49
49
|
} from "./conflict-detect";
|
|
50
50
|
import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
|
|
51
51
|
import { type OutputMeta, outputMeta } from "./output-meta";
|
|
52
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
formatPathRelativeToCwd,
|
|
54
|
+
isInternalUrlPath,
|
|
55
|
+
pathTargetsSsh,
|
|
56
|
+
peelWriteUrlSelector,
|
|
57
|
+
probeLiteralPathExists,
|
|
58
|
+
splitPathAndSel,
|
|
59
|
+
} from "./path-utils";
|
|
53
60
|
import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from "./plan-mode-guard";
|
|
54
61
|
import {
|
|
55
62
|
cachedRenderedString,
|
|
@@ -116,6 +123,43 @@ function assertWriteTargetAddressable(target: string, router: InternalUrlRouter)
|
|
|
116
123
|
);
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Fail closed when a local write target looks like a mis-dispatched read.
|
|
128
|
+
*
|
|
129
|
+
* A read-only step that selects `write` instead of `read` passes the full read
|
|
130
|
+
* expression (`src/foo.tsx:1-260:raw`) as the target. Because a literal colon
|
|
131
|
+
* filename is legal on POSIX (issue #4618), that request otherwise resolves to
|
|
132
|
+
* filesystem creation and reports success, leaving a stray zero-byte file the
|
|
133
|
+
* model cannot recover from — the local analogue of the `xd://` near-miss guard
|
|
134
|
+
* ({@link assertWriteTargetAddressable}, issue #6123).
|
|
135
|
+
*
|
|
136
|
+
* Fires only on the high-confidence combination the report identifies: the tail
|
|
137
|
+
* parses as a read-tool selector, the literal target is missing, and no content
|
|
138
|
+
* was supplied. Non-empty content is the escape hatch — it is never blocked, so
|
|
139
|
+
* a deliberate write to a selector-shaped filename still succeeds. An existing
|
|
140
|
+
* literal path or an ambiguous stat (`"unknown"`: EACCES, transient I/O) also
|
|
141
|
+
* passes through so a real file is never shadowed by the guard.
|
|
142
|
+
*/
|
|
143
|
+
function readSelectorForEmptyWrite(target: string, content: string): string | undefined {
|
|
144
|
+
if (content.length > 0) return undefined;
|
|
145
|
+
return splitPathAndSel(target).sel;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function throwReadSelectorMisfire(target: string, sel: string): never {
|
|
149
|
+
throw new ToolError(
|
|
150
|
+
`write target '${target}' ends with a read-tool selector ':${sel}' and no such file exists — refusing to create a literal file by that name. ` +
|
|
151
|
+
`If you meant to read it, use read({ path: "${target}" }). ` +
|
|
152
|
+
`If you truly intend to create this file, pass its contents in \`content\` (a non-empty write is never blocked).`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function assertNotReadSelectorMisfire(target: string, content: string, cwd: string): Promise<void> {
|
|
157
|
+
const sel = readSelectorForEmptyWrite(target, content);
|
|
158
|
+
if (sel === undefined) return;
|
|
159
|
+
if ((await probeLiteralPathExists(target, cwd)) !== "missing") return;
|
|
160
|
+
throwReadSelectorMisfire(target, sel);
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
const BULK_DIRECTIVE_RE = /^#?(\d+)\s*[:=]\s*(@ours|@theirs|@base|@both)$/;
|
|
120
164
|
/**
|
|
121
165
|
* The head of a per-id directive line — `<id>:` / `<id>=` (optionally `#`-prefixed),
|
|
@@ -576,6 +620,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
576
620
|
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
577
621
|
}
|
|
578
622
|
}
|
|
623
|
+
const writeTarget = `${resolvedArchivePath.archivePath}:${resolvedArchivePath.archiveSubPath}`;
|
|
624
|
+
const sel = readSelectorForEmptyWrite(writeTarget, content);
|
|
625
|
+
if (sel !== undefined && !entries.has(resolvedArchivePath.archiveSubPath)) {
|
|
626
|
+
throwReadSelectorMisfire(writeTarget, sel);
|
|
627
|
+
}
|
|
579
628
|
entries.set(resolvedArchivePath.archiveSubPath, content);
|
|
580
629
|
|
|
581
630
|
try {
|
|
@@ -1159,6 +1208,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
1159
1208
|
return sqliteResult;
|
|
1160
1209
|
}
|
|
1161
1210
|
|
|
1211
|
+
await assertNotReadSelectorMisfire(path, cleanContent, this.session.cwd);
|
|
1162
1212
|
enforcePlanModeWrite(this.session, path, { op: "create" });
|
|
1163
1213
|
const absolutePath = resolvePlanPath(this.session, path);
|
|
1164
1214
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
package/src/tools/xdev.ts
CHANGED
|
@@ -60,6 +60,9 @@ export const XDEV_KEEP_TOP_LEVEL: Record<string, true> = {
|
|
|
60
60
|
*/
|
|
61
61
|
export const XDEV_TRANSPORT_TOOLS: Record<string, true> = { read: true, write: true };
|
|
62
62
|
|
|
63
|
+
/** Controls which mounted-device docs are inlined into the system prompt. */
|
|
64
|
+
export type XdevDocsMode = "inline" | "builtins" | "catalog";
|
|
65
|
+
|
|
63
66
|
/**
|
|
64
67
|
* Whether an enabled tool is presented under `xd://` (rather than top-level)
|
|
65
68
|
* while the `xd://` transport is active. Discoverable tools mount unless they
|
|
@@ -170,6 +173,28 @@ function toolSummary(inst: Tool): string {
|
|
|
170
173
|
return firstLine?.trim() ?? inst.label ?? inst.name;
|
|
171
174
|
}
|
|
172
175
|
|
|
176
|
+
function promptCatalogSummary(inst: Tool, maxLength?: number): string {
|
|
177
|
+
const summary =
|
|
178
|
+
toolSummary(inst)
|
|
179
|
+
.split("\n")
|
|
180
|
+
.find(line => line.trim().length > 0)
|
|
181
|
+
?.trim() ?? inst.name;
|
|
182
|
+
if (maxLength === undefined || summary.length <= maxLength) return summary;
|
|
183
|
+
return `${summary.slice(0, maxLength).trimEnd()}…`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Compile the `tools.xdevInlineDevices` allowlist once per render, dropping
|
|
187
|
+
* non-string entries so malformed user config cannot break prompt builds. */
|
|
188
|
+
function compileInlineGlobs(patterns: readonly string[]): Bun.Glob[] {
|
|
189
|
+
if (!Array.isArray(patterns)) return [];
|
|
190
|
+
const globs: Bun.Glob[] = [];
|
|
191
|
+
for (const pattern of patterns) {
|
|
192
|
+
if (typeof pattern !== "string" || pattern.length === 0) continue;
|
|
193
|
+
globs.push(new Bun.Glob(pattern));
|
|
194
|
+
}
|
|
195
|
+
return globs;
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
/** Decode the (possibly partially streamed) inner args JSON string into display args. */
|
|
174
199
|
function decodeInnerArgs(raw: unknown): Record<string, unknown> {
|
|
175
200
|
if (typeof raw !== "string" || raw.length === 0) return {};
|
|
@@ -230,7 +255,13 @@ export class XdevRegistry {
|
|
|
230
255
|
|
|
231
256
|
/** `{name, summary}` pairs for prompt templates and /tools display. */
|
|
232
257
|
entries(): Array<{ name: string; summary: string }> {
|
|
233
|
-
return this.list().map(tool => ({
|
|
258
|
+
return this.list().map(tool => ({
|
|
259
|
+
name: tool.name,
|
|
260
|
+
summary: promptCatalogSummary(
|
|
261
|
+
tool,
|
|
262
|
+
this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined,
|
|
263
|
+
),
|
|
264
|
+
}));
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
/** `read xd://` listing with one device per line. */
|
|
@@ -273,11 +304,16 @@ export class XdevRegistry {
|
|
|
273
304
|
* Dynamic mounts embed at most {@link EXTERNAL_DESCRIPTION_CAP} description
|
|
274
305
|
* chars (schema always intact); `read xd://<tool>` returns the full text.
|
|
275
306
|
*/
|
|
276
|
-
docsAll(): string {
|
|
307
|
+
docsAll(mode: XdevDocsMode = "inline", inlinePatterns: readonly string[] = []): string {
|
|
277
308
|
const sections: string[] = [];
|
|
278
309
|
const overflow: Tool[] = [];
|
|
310
|
+
const inlineGlobs = compileInlineGlobs(inlinePatterns);
|
|
279
311
|
let used = 0;
|
|
280
312
|
for (const tool of this.list()) {
|
|
313
|
+
if (!this.#shouldInline(tool, mode, inlineGlobs)) {
|
|
314
|
+
overflow.push(tool);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
281
317
|
const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
|
|
282
318
|
const docs = renderDocs(tool, "##", descriptionCap);
|
|
283
319
|
if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET) {
|
|
@@ -291,7 +327,10 @@ export class XdevRegistry {
|
|
|
291
327
|
sections.push(
|
|
292
328
|
[
|
|
293
329
|
"## Additional devices (docs on demand)",
|
|
294
|
-
...overflow.map(tool =>
|
|
330
|
+
...overflow.map(tool => {
|
|
331
|
+
const maxLength = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
|
|
332
|
+
return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool, maxLength)}`;
|
|
333
|
+
}),
|
|
295
334
|
"",
|
|
296
335
|
`Read ${XD_URL_PREFIX}<tool> for full docs + JSON schema before first use.`,
|
|
297
336
|
].join("\n"),
|
|
@@ -300,6 +339,31 @@ export class XdevRegistry {
|
|
|
300
339
|
return sections.join("\n\n");
|
|
301
340
|
}
|
|
302
341
|
|
|
342
|
+
/** Docs for selected mounted devices under the configured prompt-doc policy. */
|
|
343
|
+
docsFor(names: Iterable<string>, mode: XdevDocsMode, inlinePatterns: readonly string[] = []): string {
|
|
344
|
+
const sections: string[] = [];
|
|
345
|
+
const inlineGlobs = compileInlineGlobs(inlinePatterns);
|
|
346
|
+
let used = 0;
|
|
347
|
+
for (const name of names) {
|
|
348
|
+
const tool = this.get(name);
|
|
349
|
+
if (!tool || !this.#shouldInline(tool, mode, inlineGlobs)) continue;
|
|
350
|
+
const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
|
|
351
|
+
const docs = renderDocs(tool, "##", descriptionCap);
|
|
352
|
+
if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET)
|
|
353
|
+
continue;
|
|
354
|
+
used += docs.length;
|
|
355
|
+
sections.push(docs);
|
|
356
|
+
}
|
|
357
|
+
return sections.join("\n\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#shouldInline(tool: Tool, mode: XdevDocsMode, inlineGlobs: readonly Bun.Glob[]): boolean {
|
|
361
|
+
return (
|
|
362
|
+
mode !== "catalog" &&
|
|
363
|
+
(mode === "inline" || this.#builtins.has(tool.name) || inlineGlobs.some(glob => glob.match(tool.name)))
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
303
367
|
#resolve(name: string): Tool {
|
|
304
368
|
const inst = this.get(name);
|
|
305
369
|
if (!inst) {
|
|
@@ -31,6 +31,15 @@ const DEFAULT_SAMPLE_RATE = 24_000;
|
|
|
31
31
|
const LEAD_SECONDS = 0.6;
|
|
32
32
|
/** Output gain applied while ducked (the user is speaking over the assistant). */
|
|
33
33
|
export const DUCK_GAIN = 0.25;
|
|
34
|
+
/**
|
|
35
|
+
* Cap on streamed PCM retained for the nonzero-exit replay. Past this the
|
|
36
|
+
* buffer is dropped: the failure being recovered is a short clip that fits the
|
|
37
|
+
* OS pipe buffer before a broken backend dies, while a backend that consumed
|
|
38
|
+
* minutes of realtime-paced audio was playing it — replaying a whole long
|
|
39
|
+
* utterance would duplicate audio, and unbounded retention (~5.8 MB/min at
|
|
40
|
+
* 24 kHz mono f32) would defeat streaming for long input.
|
|
41
|
+
*/
|
|
42
|
+
const REPLAY_RETENTION_SECONDS = 60;
|
|
34
43
|
|
|
35
44
|
/** Injection seam for {@link streamingPlayerCommandsFor} — defaults to real PATH/tools lookups. */
|
|
36
45
|
export interface StreamingPlayerLookup {
|
|
@@ -89,6 +98,20 @@ export function streamingPlayerCommandsFor(
|
|
|
89
98
|
return commands;
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Test seams for {@link StreamingAudioPlayer}: override backend discovery and
|
|
103
|
+
* the per-file fallback so playback logic can be exercised without a real audio
|
|
104
|
+
* device. Both default to the platform lookup and {@link playAudioFile}.
|
|
105
|
+
*/
|
|
106
|
+
export interface StreamingPlayerOptions {
|
|
107
|
+
/** Ordered backend commands for a sample rate; defaults to {@link streamingPlayerCommandsFor}. */
|
|
108
|
+
commandsFor?: (sampleRate: number) => PlayerCommand[];
|
|
109
|
+
/** Per-file fallback playback; defaults to {@link playAudioFile}. */
|
|
110
|
+
playAudio?: (wavPath: string, signal: AbortSignal) => Promise<void>;
|
|
111
|
+
/** Max seconds of streamed PCM retained for the nonzero-exit replay; defaults to {@link REPLAY_RETENTION_SECONDS}. */
|
|
112
|
+
replayRetentionSeconds?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
92
115
|
/**
|
|
93
116
|
* Single-session gapless player. Lifecycle: {@link start} once, {@link write}
|
|
94
117
|
* chunks in order, then {@link end} to drain or {@link stop} to abort. Not
|
|
@@ -111,6 +134,18 @@ export class StreamingAudioPlayer {
|
|
|
111
134
|
#abortController = new AbortController();
|
|
112
135
|
#wake: (() => void) | null = null;
|
|
113
136
|
#drain: Promise<void> = Promise.resolve();
|
|
137
|
+
readonly #commandsFor: (sampleRate: number) => PlayerCommand[];
|
|
138
|
+
readonly #playAudio: (wavPath: string, signal: AbortSignal) => Promise<void>;
|
|
139
|
+
readonly #replayRetentionSec: number;
|
|
140
|
+
/** Streamed PCM retained for this utterance so a failed backend can be replayed via file playback. */
|
|
141
|
+
#played: Float32Array[] = [];
|
|
142
|
+
#playedSec = 0;
|
|
143
|
+
|
|
144
|
+
constructor(options: StreamingPlayerOptions = {}) {
|
|
145
|
+
this.#commandsFor = options.commandsFor ?? (rate => streamingPlayerCommandsFor(process.platform, rate));
|
|
146
|
+
this.#playAudio = options.playAudio ?? ((wavPath, signal) => playAudioFile(wavPath, { signal }));
|
|
147
|
+
this.#replayRetentionSec = options.replayRetentionSeconds ?? REPLAY_RETENTION_SECONDS;
|
|
148
|
+
}
|
|
114
149
|
|
|
115
150
|
/** Pick a backend and begin draining. Idempotent; the first call's rate wins. */
|
|
116
151
|
start(sampleRate: number): void {
|
|
@@ -146,6 +181,7 @@ export class StreamingAudioPlayer {
|
|
|
146
181
|
if (this.#stopped) return;
|
|
147
182
|
this.#stopped = true;
|
|
148
183
|
this.#queue.length = 0;
|
|
184
|
+
this.#played.length = 0;
|
|
149
185
|
this.#abortController.abort();
|
|
150
186
|
this.#signal();
|
|
151
187
|
try {
|
|
@@ -168,7 +204,7 @@ export class StreamingAudioPlayer {
|
|
|
168
204
|
* in-flight chunk.
|
|
169
205
|
*/
|
|
170
206
|
#spawnStream(): boolean {
|
|
171
|
-
this.#candidates ??=
|
|
207
|
+
this.#candidates ??= this.#commandsFor(this.#sampleRate);
|
|
172
208
|
for (let command = this.#candidates.shift(); command; command = this.#candidates.shift()) {
|
|
173
209
|
const { cmd, args } = command;
|
|
174
210
|
try {
|
|
@@ -213,6 +249,12 @@ export class StreamingAudioPlayer {
|
|
|
213
249
|
continue;
|
|
214
250
|
}
|
|
215
251
|
if (this.#mode === "stream") {
|
|
252
|
+
if (this.#playedSec <= this.#replayRetentionSec) {
|
|
253
|
+
this.#played.push(chunk);
|
|
254
|
+
this.#playedSec += chunk.length / this.#sampleRate;
|
|
255
|
+
// Over the cap: drop retention for the rest of the utterance.
|
|
256
|
+
if (this.#playedSec > this.#replayRetentionSec) this.#played.length = 0;
|
|
257
|
+
}
|
|
216
258
|
// Pace writes so the player buffers ~LEAD_SECONDS, no more, keeping
|
|
217
259
|
// ducking and stop responsive instead of locked behind buffered audio.
|
|
218
260
|
const ahead = this.#writtenSec - (performance.now() - this.#startedAt) / 1000;
|
|
@@ -242,11 +284,27 @@ export class StreamingAudioPlayer {
|
|
|
242
284
|
try {
|
|
243
285
|
await this.#sink?.end();
|
|
244
286
|
} catch {}
|
|
245
|
-
|
|
287
|
+
const proc = this.#proc;
|
|
288
|
+
let exitCode: number | null = null;
|
|
289
|
+
if (proc) {
|
|
246
290
|
try {
|
|
247
|
-
await
|
|
291
|
+
exitCode = await proc.exited;
|
|
248
292
|
} catch {}
|
|
249
293
|
}
|
|
294
|
+
// A streaming backend that exits nonzero never opened its audio
|
|
295
|
+
// device (e.g. the bundled ffmpeg built without pulse/alsa output).
|
|
296
|
+
// For a short single-segment clip the pipe write succeeds before
|
|
297
|
+
// that death and #inputClosed is already set, so neither the
|
|
298
|
+
// broken-pipe replay nor the early-exit handler advances backends.
|
|
299
|
+
// Replay the buffered utterance through per-file playback so it
|
|
300
|
+
// still reaches the speakers.
|
|
301
|
+
if (!this.#stopped && proc && exitCode !== 0) {
|
|
302
|
+
this.#mode = "file";
|
|
303
|
+
for (const chunk of this.#played) {
|
|
304
|
+
if (this.#stopped) break;
|
|
305
|
+
await this.#playFile(chunk);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
250
308
|
}
|
|
251
309
|
} catch (error) {
|
|
252
310
|
logger.debug("tts: streaming player drain failed", {
|
|
@@ -291,7 +349,7 @@ export class StreamingAudioPlayer {
|
|
|
291
349
|
const wavPath = path.join(os.tmpdir(), `omp-speech-${Snowflake.next()}.wav`);
|
|
292
350
|
try {
|
|
293
351
|
await fs.writeFile(wavPath, encodeWav(this.#scaled(pcm), this.#sampleRate));
|
|
294
|
-
if (!this.#stopped) await
|
|
352
|
+
if (!this.#stopped) await this.#playAudio(wavPath, this.#abortController.signal);
|
|
295
353
|
} catch (error) {
|
|
296
354
|
logger.debug("tts: file playback failed", {
|
|
297
355
|
error: error instanceof Error ? error.message : String(error),
|
package/src/tts/vocalizer.ts
CHANGED
|
@@ -100,6 +100,8 @@ export class Vocalizer {
|
|
|
100
100
|
#chain: Promise<void> = Promise.resolve();
|
|
101
101
|
/** Whether the user is currently speaking; new sessions open ducked. */
|
|
102
102
|
#ducked = false;
|
|
103
|
+
/** Active scopes that suppress local TTS while another speech surface owns audio. */
|
|
104
|
+
#suspensions = 0;
|
|
103
105
|
/** Available rewrite slots; blocks queue when exhausted. */
|
|
104
106
|
#rewriteSlots = MAX_REWRITES_IN_FLIGHT;
|
|
105
107
|
#slotWaiters: Array<() => void> = [];
|
|
@@ -114,6 +116,21 @@ export class Vocalizer {
|
|
|
114
116
|
this.#enhancer = enhancer;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Suppress new vocalization until the returned idempotent release function runs.
|
|
121
|
+
* Existing synthesis and playback stop immediately; nested scopes release independently.
|
|
122
|
+
*/
|
|
123
|
+
suspend(): () => void {
|
|
124
|
+
this.#suspensions += 1;
|
|
125
|
+
this.clear();
|
|
126
|
+
let released = false;
|
|
127
|
+
return () => {
|
|
128
|
+
if (released) return;
|
|
129
|
+
released = true;
|
|
130
|
+
this.#suspensions = Math.max(0, this.#suspensions - 1);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
117
134
|
/**
|
|
118
135
|
* Stream a delta of assistant text into the pipeline. No-op when
|
|
119
136
|
* vocalization is disabled. The synthesis session (worker, player) is only
|
|
@@ -123,7 +140,7 @@ export class Vocalizer {
|
|
|
123
140
|
* pipeline (enhanced vs mechanical) is latched per utterance.
|
|
124
141
|
*/
|
|
125
142
|
pushDelta(text: string): void {
|
|
126
|
-
if (!settings.get("speech.enabled")) return;
|
|
143
|
+
if (this.#suspensions > 0 || !settings.get("speech.enabled")) return;
|
|
127
144
|
if (!text) return;
|
|
128
145
|
if (this.#enhanced || (!this.#speakable && this.#enhancer && settings.get("speech.enhanced"))) {
|
|
129
146
|
this.#pushEnhanced(text);
|
package/src/utils/jj.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as fs from "node:fs
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { $which } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { LRUCache } from "lru-cache/raw";
|
|
@@ -63,6 +63,8 @@ export class JjCommandError extends Error {
|
|
|
63
63
|
// ════════════════════════════════════════════════════════════════════════════
|
|
64
64
|
// Internal: Core execution
|
|
65
65
|
// ════════════════════════════════════════════════════════════════════════════
|
|
66
|
+
const WORKING_COPY_LABEL_REVSET = "@ | heads(::@ & bookmarks())";
|
|
67
|
+
const WORKING_COPY_LABEL_TEMPLATE = 'change_id.shortest(8) ++ "|" ++ local_bookmarks ++ "\\n"';
|
|
66
68
|
|
|
67
69
|
function ensureAvailable(): void {
|
|
68
70
|
if (!$which("jj")) {
|
|
@@ -121,6 +123,19 @@ async function runText(cwd: string, args: readonly string[], options: CommandOpt
|
|
|
121
123
|
return (await runChecked(cwd, args, options)).stdout;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
async function runOptionalText(
|
|
127
|
+
cwd: string,
|
|
128
|
+
args: readonly string[],
|
|
129
|
+
options: CommandOptions = {},
|
|
130
|
+
): Promise<string | null> {
|
|
131
|
+
try {
|
|
132
|
+
const result = await jj(cwd, args, options);
|
|
133
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
124
139
|
function splitLines(text: string): string[] {
|
|
125
140
|
return text
|
|
126
141
|
.split("\n")
|
|
@@ -135,6 +150,30 @@ function buildDiffArgs(options: DiffOptions): string[] {
|
|
|
135
150
|
return args;
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
function parseWorkingCopyLabel(raw: string): string | null {
|
|
154
|
+
let changeId: string | null = null;
|
|
155
|
+
for (const line of raw.split("\n")) {
|
|
156
|
+
const sep = line.indexOf("|");
|
|
157
|
+
const change = (sep === -1 ? line : line.slice(0, sep)).trim();
|
|
158
|
+
const bookmarks = sep === -1 ? "" : line.slice(sep + 1).trim();
|
|
159
|
+
if (changeId === null && change) changeId = change;
|
|
160
|
+
if (bookmarks) return bookmarks.replace(/\s+/g, " ");
|
|
161
|
+
}
|
|
162
|
+
return changeId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseStatusSummary(raw: string): git.GitStatusSummary {
|
|
166
|
+
let unstaged = 0;
|
|
167
|
+
let untracked = 0;
|
|
168
|
+
for (const line of raw.split("\n")) {
|
|
169
|
+
const type = line.trim()[0];
|
|
170
|
+
if (!type) continue;
|
|
171
|
+
if (type === "A") untracked++;
|
|
172
|
+
else unstaged++;
|
|
173
|
+
}
|
|
174
|
+
return { staged: 0, unstaged, untracked };
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
// ════════════════════════════════════════════════════════════════════════════
|
|
139
178
|
// Internal: Repository resolution
|
|
140
179
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -153,7 +192,16 @@ async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
|
|
|
153
192
|
// of the default workspace. Either form is a real workspace, so match on
|
|
154
193
|
// `.jj/repo` presence rather than the inner `store/` directory.
|
|
155
194
|
try {
|
|
156
|
-
await fs.stat(path.join(dir, ".jj", "repo"));
|
|
195
|
+
await fs.promises.stat(path.join(dir, ".jj", "repo"));
|
|
196
|
+
return true;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function hasJjWorkspaceMetadataSync(dir: string): boolean {
|
|
203
|
+
try {
|
|
204
|
+
fs.statSync(path.join(dir, ".jj", "repo"));
|
|
157
205
|
return true;
|
|
158
206
|
} catch {
|
|
159
207
|
return false;
|
|
@@ -180,6 +228,21 @@ async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
|
180
228
|
return undefined;
|
|
181
229
|
}
|
|
182
230
|
|
|
231
|
+
function findWorkspaceRootSync(cwd: string): string | undefined {
|
|
232
|
+
const key = path.resolve(cwd);
|
|
233
|
+
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key)?.root;
|
|
234
|
+
|
|
235
|
+
for (let dir: string | undefined = key; dir; dir = parentOf(dir)) {
|
|
236
|
+
if (hasJjWorkspaceMetadataSync(dir)) {
|
|
237
|
+
workspaceRootCache.set(key, { root: dir });
|
|
238
|
+
return dir;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
workspaceRootCache.set(key, {});
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
183
246
|
/**
|
|
184
247
|
* Resolve the `.jj/repo` directory backing a workspace root, following the file
|
|
185
248
|
* indirection used by non-default workspaces. `jj workspace add` writes a FILE at
|
|
@@ -190,8 +253,8 @@ async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
|
190
253
|
async function resolveRepoDir(root: string): Promise<string> {
|
|
191
254
|
const jjDir = path.join(root, ".jj");
|
|
192
255
|
const repoPath = path.join(jjDir, "repo");
|
|
193
|
-
if ((await fs.stat(repoPath)).isFile()) {
|
|
194
|
-
const target = (await fs.readFile(repoPath, "utf8")).trim();
|
|
256
|
+
if ((await fs.promises.stat(repoPath)).isFile()) {
|
|
257
|
+
const target = (await fs.promises.readFile(repoPath, "utf8")).trim();
|
|
195
258
|
return path.resolve(jjDir, target);
|
|
196
259
|
}
|
|
197
260
|
return repoPath;
|
|
@@ -221,6 +284,56 @@ export const diff = Object.assign(
|
|
|
221
284
|
},
|
|
222
285
|
);
|
|
223
286
|
|
|
287
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
288
|
+
// API: working copy
|
|
289
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
290
|
+
|
|
291
|
+
/** Jujutsu working-copy metadata used by status displays. */
|
|
292
|
+
export const workingCopy = {
|
|
293
|
+
/**
|
|
294
|
+
* Label `@` with its nearest bookmark, falling back to its short change ID.
|
|
295
|
+
* Returns `null` when `jj` is unavailable or the query fails.
|
|
296
|
+
*/
|
|
297
|
+
async label(cwd: string, signal?: AbortSignal): Promise<string | null> {
|
|
298
|
+
const raw = await runOptionalText(
|
|
299
|
+
cwd,
|
|
300
|
+
[
|
|
301
|
+
"log",
|
|
302
|
+
"--no-graph",
|
|
303
|
+
"--ignore-working-copy",
|
|
304
|
+
"-r",
|
|
305
|
+
WORKING_COPY_LABEL_REVSET,
|
|
306
|
+
"-T",
|
|
307
|
+
WORKING_COPY_LABEL_TEMPLATE,
|
|
308
|
+
],
|
|
309
|
+
{ signal },
|
|
310
|
+
);
|
|
311
|
+
return raw === null ? null : parseWorkingCopyLabel(raw);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/** Parse working-copy label query output. */
|
|
315
|
+
parseLabel: parseWorkingCopyLabel,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
319
|
+
// API: status
|
|
320
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
321
|
+
|
|
322
|
+
/** Jujutsu working-copy status derived from the changes in `@`. */
|
|
323
|
+
export const status = {
|
|
324
|
+
/**
|
|
325
|
+
* Count changes in `@` relative to its parent using the Git status shape.
|
|
326
|
+
* Jujutsu has no index, so `staged` is always zero.
|
|
327
|
+
*/
|
|
328
|
+
async summary(cwd: string, signal?: AbortSignal): Promise<git.GitStatusSummary | null> {
|
|
329
|
+
const raw = await runOptionalText(cwd, ["diff", "-r", "@", "--summary", "--ignore-working-copy"], { signal });
|
|
330
|
+
return raw === null ? null : parseStatusSummary(raw);
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/** Parse `jj diff --summary` output into status counts. */
|
|
334
|
+
parse: parseStatusSummary,
|
|
335
|
+
};
|
|
336
|
+
|
|
224
337
|
// ════════════════════════════════════════════════════════════════════════════
|
|
225
338
|
// API: repo
|
|
226
339
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -231,6 +344,14 @@ export const repo = {
|
|
|
231
344
|
workspaceRootCache.clear();
|
|
232
345
|
},
|
|
233
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Resolve the current workspace root synchronously from on-disk metadata.
|
|
349
|
+
* Intended for render paths that cannot await filesystem I/O.
|
|
350
|
+
*/
|
|
351
|
+
rootSync(cwd: string): string | null {
|
|
352
|
+
return findWorkspaceRootSync(cwd) ?? null;
|
|
353
|
+
},
|
|
354
|
+
|
|
234
355
|
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
|
235
356
|
async root(cwd: string): Promise<string | null> {
|
|
236
357
|
return (await findWorkspaceRoot(cwd)) ?? null;
|
|
@@ -102,6 +102,9 @@ const EXTENSION_LANG: Record<string, readonly [string, string]> = {
|
|
|
102
102
|
md: ["markdown", "markdown"],
|
|
103
103
|
markdown: ["markdown", "markdown"],
|
|
104
104
|
mdx: ["markdown", "markdown"],
|
|
105
|
+
mdc: ["markdown", "markdown"],
|
|
106
|
+
mkd: ["markdown", "markdown"],
|
|
107
|
+
mdown: ["markdown", "markdown"],
|
|
105
108
|
rst: ["restructuredtext", "restructuredtext"],
|
|
106
109
|
adoc: ["asciidoc", "asciidoc"],
|
|
107
110
|
tex: ["latex", "latex"],
|
|
@@ -217,6 +220,10 @@ export function getLanguageFromPath(filePath: string): string | undefined {
|
|
|
217
220
|
return undefined;
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
export function isMarkdownPath(filePath: string): boolean {
|
|
224
|
+
return getLanguageFromPath(filePath) === "markdown";
|
|
225
|
+
}
|
|
226
|
+
|
|
220
227
|
/**
|
|
221
228
|
* LSP language identifier; falls back to `plaintext`.
|
|
222
229
|
*/
|