@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.9
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 +91 -0
- package/dist/cli.js +5025 -4930
- package/dist/types/config/__tests__/model-registry.test.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +28 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +50 -3
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +1 -1
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +6 -2
- package/dist/types/mcp/render.d.ts +2 -2
- package/dist/types/mcp/tool-bridge.d.ts +16 -3
- package/dist/types/mcp/types.d.ts +6 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +16 -0
- package/dist/types/modes/rpc/rpc-messages.d.ts +26 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +40 -0
- package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/types.d.ts +9 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +1 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +86 -3
- package/dist/types/session/session-loader.d.ts +6 -4
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/task/types.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/hub/jobs.d.ts +1 -1
- package/dist/types/tools/index.d.ts +3 -0
- package/dist/types/tools/report-tool-issue.d.ts +24 -9
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/dist/types/web/search/providers/firecrawl.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +1 -1
- package/src/cli/grievances-cli.ts +2 -2
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/cli/usage-cli.ts +1 -1
- package/src/config/__tests__/model-registry.test.ts +147 -0
- package/src/config/model-registry.ts +87 -24
- package/src/config/model-resolver.ts +56 -32
- package/src/config/settings-schema.ts +46 -3
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +1 -0
- package/src/extensibility/legacy-pi-tui-shim.ts +10 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +851 -310
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/internal-urls/registry-helpers.ts +40 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/client.ts +23 -0
- package/src/lsp/clients/biome-client.ts +3 -4
- package/src/lsp/index.ts +26 -8
- package/src/main.ts +16 -5
- package/src/mcp/manager.ts +39 -8
- package/src/mcp/render.ts +94 -35
- package/src/mcp/tool-bridge.ts +116 -11
- package/src/mcp/types.ts +7 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/agent-hub.ts +26 -9
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/oauth-selector.ts +24 -7
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/settings-defs.ts +5 -2
- package/src/modes/components/settings-selector.ts +9 -5
- package/src/modes/components/tool-execution.test.ts +63 -2
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +36 -12
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/mcp-command-controller.ts +70 -40
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +39 -4
- package/src/modes/rpc/rpc-client.ts +94 -3
- package/src/modes/rpc/rpc-frame.ts +164 -4
- package/src/modes/rpc/rpc-messages.ts +127 -0
- package/src/modes/rpc/rpc-mode.ts +79 -7
- package/src/modes/rpc/rpc-types.ts +34 -2
- package/src/modes/setup-wizard/scenes/model.ts +5 -7
- package/src/modes/setup-wizard/scenes/providers.ts +3 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +8 -2
- package/src/modes/setup-wizard/scenes/theme.ts +13 -3
- package/src/modes/setup-wizard/scenes/types.ts +9 -1
- package/src/modes/setup-wizard/scenes/web-search.ts +6 -1
- package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/goals/guided-goal-system.md +24 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/task.md +12 -2
- package/src/sdk.ts +179 -51
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +351 -46
- package/src/session/session-context.test.ts +224 -1
- package/src/session/session-context.ts +41 -2
- package/src/session/session-loader.ts +10 -5
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +44 -27
- package/src/task/index.ts +63 -8
- package/src/task/prewalk.ts +6 -0
- package/src/task/structured-subagent.ts +3 -1
- package/src/task/types.ts +16 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +67 -8
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/report-tool-issue.ts +79 -28
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
- package/src/web/search/providers/firecrawl.ts +46 -13
- package/src/web/search/types.ts +5 -1
|
@@ -782,7 +782,6 @@ function parseModelPatternWithContext(
|
|
|
782
782
|
options?: { allowInvalidThinkingSelectorFallback?: boolean },
|
|
783
783
|
): ParsedModelResult {
|
|
784
784
|
// Exact match on the full pattern first (no fuzzy): a literal id that
|
|
785
|
-
// contains a colon (`coding-router:max`) wins over any suffix split.
|
|
786
785
|
const exactMatch = matchModel(pattern, availableModels, context, { exactOnly: true });
|
|
787
786
|
if (exactMatch) {
|
|
788
787
|
return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
|
|
@@ -1598,11 +1597,33 @@ export function filterAvailableModelsByEnabledPatterns(
|
|
|
1598
1597
|
|
|
1599
1598
|
return includeSyntheticAllowedModels(available, allowedModels);
|
|
1600
1599
|
}
|
|
1600
|
+
function findExactCliModel(
|
|
1601
|
+
selector: string,
|
|
1602
|
+
allModels: Model<Api>[],
|
|
1603
|
+
availableModels: Model<Api>[],
|
|
1604
|
+
): Model<Api> | undefined {
|
|
1605
|
+
// Explicit provider/id references stay authoritative against the full catalog.
|
|
1606
|
+
const referenced = findExactModelReferenceMatch(selector, allModels);
|
|
1607
|
+
if (referenced) return referenced;
|
|
1608
|
+
|
|
1609
|
+
// Flat-id (or full-selector-string) matches prefer authenticated providers,
|
|
1610
|
+
// then fall back to catalog order. This covers aggregator-style flat ids
|
|
1611
|
+
// that merely look provider-qualified (e.g. "openai/gpt-oss-120b" hosted on
|
|
1612
|
+
// OpenRouter), where the provider/id decomposition above found nothing.
|
|
1613
|
+
const lower = selector.toLowerCase();
|
|
1614
|
+
const isFlatMatch = (model: Model<Api>) =>
|
|
1615
|
+
model.id.toLowerCase() === lower || formatModelString(model).toLowerCase() === lower;
|
|
1616
|
+
const preferred = availableModels.find(isFlatMatch);
|
|
1617
|
+
if (preferred) return preferred;
|
|
1618
|
+
return availableModels === allModels ? undefined : allModels.find(isFlatMatch);
|
|
1619
|
+
}
|
|
1601
1620
|
|
|
1602
1621
|
export interface ResolveCliModelResult {
|
|
1603
1622
|
model: Model<Api> | undefined;
|
|
1604
|
-
/** configuredPatterns
|
|
1623
|
+
/** configuredPatterns contains the role's ordered primary candidates. */
|
|
1605
1624
|
configuredPatterns?: string[];
|
|
1625
|
+
/** configuredRole identifies the role expanded into configuredPatterns. */
|
|
1626
|
+
configuredRole?: string;
|
|
1606
1627
|
/** configuredPatternIndex identifies the configured role pattern that matched an available model. */
|
|
1607
1628
|
configuredPatternIndex?: number;
|
|
1608
1629
|
selector?: string;
|
|
@@ -1620,17 +1641,19 @@ export function resolveCliModel(options: {
|
|
|
1620
1641
|
cliProvider?: string;
|
|
1621
1642
|
cliModel?: string;
|
|
1622
1643
|
modelRegistry: CliModelRegistry;
|
|
1644
|
+
/** Authenticated models to prefer for unqualified selectors; omit to preserve catalog-order behavior. */
|
|
1645
|
+
availableModels?: Model<Api>[];
|
|
1623
1646
|
settings?: Settings;
|
|
1624
1647
|
preferences?: ModelMatchPreferences;
|
|
1625
1648
|
}): ResolveCliModelResult {
|
|
1626
|
-
const { cliProvider, cliModel, modelRegistry, settings, preferences } = options;
|
|
1649
|
+
const { cliProvider, cliModel, modelRegistry, settings, preferences, availableModels: preferredModels } = options;
|
|
1627
1650
|
|
|
1628
1651
|
if (!cliModel) {
|
|
1629
1652
|
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
|
1630
1653
|
}
|
|
1631
1654
|
|
|
1632
|
-
const
|
|
1633
|
-
if (
|
|
1655
|
+
const allModels = modelRegistry.getAll();
|
|
1656
|
+
if (allModels.length === 0) {
|
|
1634
1657
|
return {
|
|
1635
1658
|
model: undefined,
|
|
1636
1659
|
selector: undefined,
|
|
@@ -1639,8 +1662,9 @@ export function resolveCliModel(options: {
|
|
|
1639
1662
|
};
|
|
1640
1663
|
}
|
|
1641
1664
|
|
|
1665
|
+
const availableModels = preferredModels ?? allModels;
|
|
1642
1666
|
const providerMap = new Map<string, string>();
|
|
1643
|
-
for (const model of
|
|
1667
|
+
for (const model of allModels) {
|
|
1644
1668
|
providerMap.set(model.provider.toLowerCase(), model.provider);
|
|
1645
1669
|
}
|
|
1646
1670
|
|
|
@@ -1656,19 +1680,7 @@ export function resolveCliModel(options: {
|
|
|
1656
1680
|
|
|
1657
1681
|
const trimmedModel = cliModel.trim();
|
|
1658
1682
|
if (!provider) {
|
|
1659
|
-
const
|
|
1660
|
-
// When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
|
|
1661
|
-
// provider+id match over flat id match. Without this, a model with id
|
|
1662
|
-
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
1663
|
-
// with id "glm-5", because Array.find returns the first catalog hit.
|
|
1664
|
-
let exact = findExactModelReferenceMatch(trimmedModel, availableModels);
|
|
1665
|
-
if (!exact) {
|
|
1666
|
-
// Flat exact id (or full selector) by catalog order: CLI resolution
|
|
1667
|
-
// stays deterministic across runs regardless of usage-based ranking.
|
|
1668
|
-
exact = availableModels.find(
|
|
1669
|
-
model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1683
|
+
const exact = findExactCliModel(trimmedModel, allModels, availableModels);
|
|
1672
1684
|
if (exact) {
|
|
1673
1685
|
return {
|
|
1674
1686
|
model: exact,
|
|
@@ -1684,15 +1696,7 @@ export function resolveCliModel(options: {
|
|
|
1684
1696
|
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1685
1697
|
);
|
|
1686
1698
|
if (exactThinkingLevel) {
|
|
1687
|
-
|
|
1688
|
-
if (!exactSuffixed) {
|
|
1689
|
-
const lowerExactBase = exactBase.toLowerCase();
|
|
1690
|
-
exactSuffixed = availableModels.find(
|
|
1691
|
-
model =>
|
|
1692
|
-
model.id.toLowerCase() === lowerExactBase ||
|
|
1693
|
-
`${model.provider}/${model.id}`.toLowerCase() === lowerExactBase,
|
|
1694
|
-
);
|
|
1695
|
-
}
|
|
1699
|
+
const exactSuffixed = findExactCliModel(exactBase, allModels, availableModels);
|
|
1696
1700
|
if (exactSuffixed) {
|
|
1697
1701
|
return {
|
|
1698
1702
|
model: exactSuffixed,
|
|
@@ -1718,15 +1722,28 @@ export function resolveCliModel(options: {
|
|
|
1718
1722
|
? `${formatModelRoleAlias(bareRoleName)}${bareRoleThinkingLevel ? `:${bareRoleThinkingLevel}` : ""}`
|
|
1719
1723
|
: undefined;
|
|
1720
1724
|
if (roleSelector) {
|
|
1725
|
+
const { base: roleAlias } = splitThinkingSuffix(
|
|
1726
|
+
roleSelector,
|
|
1727
|
+
modelRoleAliasPrefixLength(roleSelector) ?? -1,
|
|
1728
|
+
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1729
|
+
);
|
|
1730
|
+
const configuredRole = getModelRoleAlias(roleAlias, settings);
|
|
1721
1731
|
configuredPatterns = resolveConfiguredModelPatterns([roleSelector], settings);
|
|
1722
|
-
const
|
|
1732
|
+
const availableResolved = resolveModelRoleValue(roleSelector, availableModels, {
|
|
1723
1733
|
settings,
|
|
1724
1734
|
matchPreferences: preferences,
|
|
1725
1735
|
});
|
|
1736
|
+
const resolved = availableResolved.model
|
|
1737
|
+
? availableResolved
|
|
1738
|
+
: resolveModelRoleValue(roleSelector, allModels, {
|
|
1739
|
+
settings,
|
|
1740
|
+
matchPreferences: preferences,
|
|
1741
|
+
});
|
|
1726
1742
|
if (resolved.model) {
|
|
1727
1743
|
return {
|
|
1728
1744
|
model: resolved.model,
|
|
1729
1745
|
selector: formatModelString(resolved.model),
|
|
1746
|
+
configuredRole,
|
|
1730
1747
|
configuredPatterns,
|
|
1731
1748
|
configuredPatternIndex: resolved.matchedPatternIndex,
|
|
1732
1749
|
thinkingLevel: resolved.thinkingLevel,
|
|
@@ -1738,6 +1755,7 @@ export function resolveCliModel(options: {
|
|
|
1738
1755
|
return {
|
|
1739
1756
|
model: undefined,
|
|
1740
1757
|
configuredPatterns,
|
|
1758
|
+
configuredRole,
|
|
1741
1759
|
selector: undefined,
|
|
1742
1760
|
thinkingLevel: undefined,
|
|
1743
1761
|
warning: resolved.warning,
|
|
@@ -1767,7 +1785,7 @@ export function resolveCliModel(options: {
|
|
|
1767
1785
|
}
|
|
1768
1786
|
|
|
1769
1787
|
if (provider) {
|
|
1770
|
-
const exactProviderMatch = resolveProviderModelReference(provider, pattern,
|
|
1788
|
+
const exactProviderMatch = resolveProviderModelReference(provider, pattern, allModels);
|
|
1771
1789
|
if (exactProviderMatch) {
|
|
1772
1790
|
return {
|
|
1773
1791
|
model: exactProviderMatch,
|
|
@@ -1779,10 +1797,16 @@ export function resolveCliModel(options: {
|
|
|
1779
1797
|
}
|
|
1780
1798
|
}
|
|
1781
1799
|
|
|
1782
|
-
const candidates = provider ?
|
|
1783
|
-
|
|
1800
|
+
const candidates = provider ? allModels.filter(model => model.provider === provider) : availableModels;
|
|
1801
|
+
let parsed = parseModelPattern(pattern, candidates, preferences, {
|
|
1784
1802
|
allowInvalidThinkingSelectorFallback: false,
|
|
1785
1803
|
});
|
|
1804
|
+
if (!parsed.model && !provider) {
|
|
1805
|
+
parsed = parseModelPattern(pattern, allModels, preferences, {
|
|
1806
|
+
allowInvalidThinkingSelectorFallback: false,
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
const { model, thinkingLevel, warning, upstream } = parsed;
|
|
1786
1810
|
|
|
1787
1811
|
if (!model) {
|
|
1788
1812
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
@@ -207,6 +207,8 @@ interface UiNumber extends UiBase {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
interface UiString extends UiBase {
|
|
210
|
+
/** Mask the value in both the settings row and text editor. */
|
|
211
|
+
secret?: boolean;
|
|
210
212
|
/**
|
|
211
213
|
* Submenu options.
|
|
212
214
|
* - Array → submenu with these choices.
|
|
@@ -219,6 +221,7 @@ interface UiString extends UiBase {
|
|
|
219
221
|
/** Wide ui shape exposed to consumers that walk the schema generically. */
|
|
220
222
|
export type AnyUiMetadata = UiBase & {
|
|
221
223
|
options?: ReadonlyArray<SubmenuOption> | "runtime";
|
|
224
|
+
secret?: boolean;
|
|
222
225
|
};
|
|
223
226
|
|
|
224
227
|
interface BooleanDef {
|
|
@@ -2753,7 +2756,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
2753
2756
|
},
|
|
2754
2757
|
},
|
|
2755
2758
|
|
|
2756
|
-
"hindsight.apiToken": {
|
|
2759
|
+
"hindsight.apiToken": {
|
|
2760
|
+
type: "string",
|
|
2761
|
+
default: undefined,
|
|
2762
|
+
ui: {
|
|
2763
|
+
tab: "memory",
|
|
2764
|
+
group: "Hindsight",
|
|
2765
|
+
label: "Hindsight API Token",
|
|
2766
|
+
description: "Bearer token for authenticated Hindsight servers",
|
|
2767
|
+
condition: "hindsightActive",
|
|
2768
|
+
secret: true,
|
|
2769
|
+
},
|
|
2770
|
+
},
|
|
2757
2771
|
|
|
2758
2772
|
"hindsight.bankId": {
|
|
2759
2773
|
type: "string",
|
|
@@ -2861,6 +2875,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
2861
2875
|
|
|
2862
2876
|
"hindsight.debug": { type: "boolean", default: false },
|
|
2863
2877
|
|
|
2878
|
+
"hindsight.requestTimeoutMs": { type: "number", default: 30_000 },
|
|
2879
|
+
"hindsight.reflectTimeoutMs": { type: "number", default: 120_000 },
|
|
2880
|
+
"hindsight.recallTimeoutMs": { type: "number", default: 30_000 },
|
|
2881
|
+
"hindsight.retainTimeoutMs": { type: "number", default: 60_000 },
|
|
2882
|
+
|
|
2864
2883
|
"hindsight.mentalModelsEnabled": {
|
|
2865
2884
|
type: "boolean",
|
|
2866
2885
|
default: true,
|
|
@@ -3940,6 +3959,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
3940
3959
|
},
|
|
3941
3960
|
},
|
|
3942
3961
|
|
|
3962
|
+
"mcp.renderMarkdownResults": {
|
|
3963
|
+
type: "boolean",
|
|
3964
|
+
default: true,
|
|
3965
|
+
ui: {
|
|
3966
|
+
tab: "tools",
|
|
3967
|
+
group: "Discovery & MCP",
|
|
3968
|
+
label: "MCP Markdown Results",
|
|
3969
|
+
description: "Render non-JSON MCP text results as Markdown in the transcript",
|
|
3970
|
+
},
|
|
3971
|
+
},
|
|
3972
|
+
|
|
3943
3973
|
"mcp.notifications": {
|
|
3944
3974
|
type: "boolean",
|
|
3945
3975
|
default: false,
|
|
@@ -4084,6 +4114,18 @@ export const SETTINGS_SCHEMA = {
|
|
|
4084
4114
|
},
|
|
4085
4115
|
},
|
|
4086
4116
|
|
|
4117
|
+
"task.isolation.apply": {
|
|
4118
|
+
type: "boolean",
|
|
4119
|
+
default: true,
|
|
4120
|
+
ui: {
|
|
4121
|
+
tab: "tasks",
|
|
4122
|
+
group: "Isolation",
|
|
4123
|
+
label: "Apply Isolated Changes",
|
|
4124
|
+
description:
|
|
4125
|
+
"Automatically apply successful isolated task changes to the parent checkout; disable to retain patch or branch artifacts",
|
|
4126
|
+
},
|
|
4127
|
+
},
|
|
4128
|
+
|
|
4087
4129
|
"task.isolation.merge": {
|
|
4088
4130
|
type: "enum",
|
|
4089
4131
|
values: ["patch", "branch"] as const,
|
|
@@ -5027,12 +5069,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
5027
5069
|
|
|
5028
5070
|
"dev.autoqa": {
|
|
5029
5071
|
type: "boolean",
|
|
5030
|
-
default:
|
|
5072
|
+
default: true,
|
|
5031
5073
|
ui: {
|
|
5032
5074
|
tab: "tools",
|
|
5033
5075
|
group: "Developer",
|
|
5034
5076
|
label: "Auto QA",
|
|
5035
|
-
description:
|
|
5077
|
+
description:
|
|
5078
|
+
"Automated tool issue reporting (xd://report_issue). On by default; the first report asks for consent, and denying it disables reporting until re-enabled explicitly",
|
|
5036
5079
|
},
|
|
5037
5080
|
},
|
|
5038
5081
|
|
package/src/config/settings.ts
CHANGED
|
@@ -330,6 +330,8 @@ export class Settings {
|
|
|
330
330
|
#modified = new Set<string>();
|
|
331
331
|
/** Individual project model roles modified during this session */
|
|
332
332
|
#modifiedProjectModelRoles = new Set<string>();
|
|
333
|
+
/** Individual global model roles modified during this session (for partial save) */
|
|
334
|
+
#modifiedGlobalModelRoles = new Set<string>();
|
|
333
335
|
/**
|
|
334
336
|
* Original process-wide model-role overrides captured before a project edit
|
|
335
337
|
* temporarily replaced them via `#updateRuntimeModelRoleOverride`. Restored
|
|
@@ -551,7 +553,7 @@ export class Settings {
|
|
|
551
553
|
if (this.#projectSavePromise) {
|
|
552
554
|
await this.#projectSavePromise;
|
|
553
555
|
}
|
|
554
|
-
if (this.#modified.size > 0) {
|
|
556
|
+
if (this.#modified.size > 0 || this.#modifiedGlobalModelRoles.size > 0) {
|
|
555
557
|
await this.#saveNow();
|
|
556
558
|
}
|
|
557
559
|
if (this.#modifiedProjectModelRoles.size > 0) {
|
|
@@ -833,13 +835,22 @@ export class Settings {
|
|
|
833
835
|
* stale skip in place.
|
|
834
836
|
*/
|
|
835
837
|
setModelRole(role: ModelRole | string, modelId: string | undefined): void {
|
|
838
|
+
const prev = this.get("modelRoles");
|
|
836
839
|
const current = this.#modelRolesFromLayer(this.#global);
|
|
837
840
|
if (modelId === undefined) {
|
|
838
841
|
delete current[role];
|
|
839
842
|
} else {
|
|
840
843
|
current[role] = modelId;
|
|
841
844
|
}
|
|
842
|
-
|
|
845
|
+
// Persist per-role rather than marking the whole `modelRoles` path
|
|
846
|
+
// modified: #saveNow merges only the changed role into the re-read
|
|
847
|
+
// file, so a concurrent external edit to a sibling role is not
|
|
848
|
+
// clobbered by this process's stale in-memory snapshot.
|
|
849
|
+
setByPath(this.#global, ["modelRoles"], current);
|
|
850
|
+
this.#modifiedGlobalModelRoles.add(role);
|
|
851
|
+
this.#rebuildMerged();
|
|
852
|
+
this.#queueSave();
|
|
853
|
+
this.#fireEffectiveSettingChanged("modelRoles", this.get("modelRoles"), prev);
|
|
843
854
|
if (this.isProjectModelRoleRuntimeOverrideActive(role)) {
|
|
844
855
|
return;
|
|
845
856
|
}
|
|
@@ -1619,7 +1630,8 @@ export class Settings {
|
|
|
1619
1630
|
clearTimeout(this.#saveTimer);
|
|
1620
1631
|
this.#saveTimer = setTimeout(() => {
|
|
1621
1632
|
this.#saveTimer = undefined;
|
|
1622
|
-
const
|
|
1633
|
+
const previousSave = this.#savePromise;
|
|
1634
|
+
const savePromise = previousSave ? previousSave.then(() => this.#saveNow()) : this.#saveNow();
|
|
1623
1635
|
this.#savePromise = savePromise;
|
|
1624
1636
|
savePromise
|
|
1625
1637
|
.catch(err => {
|
|
@@ -1634,27 +1646,75 @@ export class Settings {
|
|
|
1634
1646
|
}
|
|
1635
1647
|
|
|
1636
1648
|
async #saveNow(): Promise<void> {
|
|
1637
|
-
if (!this.#persist || !this.#configPath
|
|
1649
|
+
if (!this.#persist || !this.#configPath) return;
|
|
1650
|
+
if (this.#modified.size === 0 && this.#modifiedGlobalModelRoles.size === 0) return;
|
|
1638
1651
|
|
|
1639
1652
|
const configPath = this.#configPath;
|
|
1640
1653
|
const modifiedPaths = [...this.#modified];
|
|
1654
|
+
const modifiedModelRoles = [...this.#modifiedGlobalModelRoles];
|
|
1655
|
+
const globalRolesAtStart = this.#modelRolesFromLayer(this.#global);
|
|
1641
1656
|
this.#modified.clear();
|
|
1657
|
+
this.#modifiedGlobalModelRoles.clear();
|
|
1642
1658
|
|
|
1643
1659
|
try {
|
|
1644
1660
|
await withFileLock(configPath, async () => {
|
|
1645
1661
|
// Re-read to preserve external changes
|
|
1646
1662
|
const current = await this.#loadYaml(configPath);
|
|
1647
1663
|
|
|
1648
|
-
// Apply only our modified paths
|
|
1664
|
+
// Apply only our modified whole-value paths
|
|
1649
1665
|
for (const modPath of modifiedPaths) {
|
|
1650
1666
|
const segments = modPath.split(".");
|
|
1651
1667
|
const value = getByPath(this.#global, segments);
|
|
1652
1668
|
setByPath(current, segments, value);
|
|
1653
1669
|
}
|
|
1654
1670
|
|
|
1671
|
+
// Merge only the model roles captured by this save. Then retain
|
|
1672
|
+
// any role changed while the async read/lock was pending before
|
|
1673
|
+
// replacing #global, so the follow-up save still sees its value.
|
|
1674
|
+
const latestGlobalRoles = this.#modelRolesFromLayer(this.#global);
|
|
1675
|
+
const rolesToPreserve = new Set(this.#modifiedGlobalModelRoles);
|
|
1676
|
+
for (const role in globalRolesAtStart) {
|
|
1677
|
+
if (globalRolesAtStart[role] !== latestGlobalRoles[role]) {
|
|
1678
|
+
rolesToPreserve.add(role);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const role in latestGlobalRoles) {
|
|
1682
|
+
if (globalRolesAtStart[role] !== latestGlobalRoles[role]) {
|
|
1683
|
+
rolesToPreserve.add(role);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (modifiedModelRoles.length > 0 || rolesToPreserve.size > 0) {
|
|
1687
|
+
const currentRoles = getByPath(current, ["modelRoles"]);
|
|
1688
|
+
const mergedRoles: Record<string, unknown> = isRecord(currentRoles) ? { ...currentRoles } : {};
|
|
1689
|
+
for (const role of modifiedModelRoles) {
|
|
1690
|
+
if (Object.hasOwn(globalRolesAtStart, role)) {
|
|
1691
|
+
mergedRoles[role] = globalRolesAtStart[role];
|
|
1692
|
+
} else {
|
|
1693
|
+
delete mergedRoles[role];
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
for (const role of rolesToPreserve) {
|
|
1697
|
+
if (Object.hasOwn(latestGlobalRoles, role)) {
|
|
1698
|
+
mergedRoles[role] = latestGlobalRoles[role];
|
|
1699
|
+
} else {
|
|
1700
|
+
delete mergedRoles[role];
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
setByPath(current, ["modelRoles"], mergedRoles);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1655
1706
|
// Update our global with any external changes we preserved
|
|
1656
1707
|
this.#global = current;
|
|
1657
1708
|
await Bun.write(configPath, YAML.stringify(this.#global, null, 2));
|
|
1709
|
+
// These pending roles were included in this write. Remove each
|
|
1710
|
+
// only if no newer local change arrived while Bun.write was in
|
|
1711
|
+
// flight; a newer value still needs the queued follow-up save.
|
|
1712
|
+
const globalRolesAfterWrite = this.#modelRolesFromLayer(this.#global);
|
|
1713
|
+
for (const role of rolesToPreserve) {
|
|
1714
|
+
if (latestGlobalRoles[role] === globalRolesAfterWrite[role]) {
|
|
1715
|
+
this.#modifiedGlobalModelRoles.delete(role);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1658
1718
|
});
|
|
1659
1719
|
} catch (error) {
|
|
1660
1720
|
logger.warn("Settings: save failed", { error: String(error) });
|
|
@@ -1662,6 +1722,9 @@ export class Settings {
|
|
|
1662
1722
|
for (const p of modifiedPaths) {
|
|
1663
1723
|
this.#modified.add(p);
|
|
1664
1724
|
}
|
|
1725
|
+
for (const role of modifiedModelRoles) {
|
|
1726
|
+
this.#modifiedGlobalModelRoles.add(role);
|
|
1727
|
+
}
|
|
1665
1728
|
}
|
|
1666
1729
|
|
|
1667
1730
|
this.#rebuildMerged();
|
package/src/dap/client.ts
CHANGED
|
@@ -75,6 +75,10 @@ export class DapClient {
|
|
|
75
75
|
#reverseRequestHandlers = new Map<string, DapReverseRequestHandler>();
|
|
76
76
|
#adapterExited = false;
|
|
77
77
|
#pendingWriteExitRejectors = new Set<() => void>();
|
|
78
|
+
/** Rejectors for in-flight {@link waitForEvent} calls, woken when the
|
|
79
|
+
* transport closes so an event that can never arrive fails fast instead of
|
|
80
|
+
* waiting out its own timeout. */
|
|
81
|
+
#eventWaiterRejectors = new Set<(error: Error) => void>();
|
|
78
82
|
|
|
79
83
|
constructor(
|
|
80
84
|
adapter: DapResolvedAdapter,
|
|
@@ -188,12 +192,17 @@ export class DapClient {
|
|
|
188
192
|
});
|
|
189
193
|
|
|
190
194
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
// Wait for the adapter to announce it is listening on `port` before
|
|
196
|
+
// connecting. Without this gate the first connect can land in the
|
|
197
|
+
// window where a just-released reservation port still accepts
|
|
198
|
+
// connections (WSL2 mirrored networking, issue #6055): the transport
|
|
199
|
+
// then binds to a ghost of the reservation listener instead of the
|
|
200
|
+
// adapter. Draining stdout here also avoids a pipe-buffer deadlock —
|
|
201
|
+
// in tcp mode the DAP protocol flows over the socket, so nothing else
|
|
202
|
+
// consumes the adapter's stdout.
|
|
203
|
+
const readyTimeoutMs = socketReadyTimeoutMs ?? SOCKET_READY_TIMEOUT_MS;
|
|
204
|
+
await waitForTcpServerListening(proc, port, readyTimeoutMs);
|
|
205
|
+
const { readable, writeSink, socket } = await waitForTcpTransport(host, port, readyTimeoutMs, proc);
|
|
197
206
|
const client = new DapClient(adapter, cwd, proc, { readable, writeSink, socket, port });
|
|
198
207
|
proc.exited.then(() => client.#handleProcessExit());
|
|
199
208
|
void client.#startMessageReader();
|
|
@@ -389,6 +398,7 @@ export class DapClient {
|
|
|
389
398
|
let timeout: NodeJS.Timeout | undefined;
|
|
390
399
|
const cleanup = () => {
|
|
391
400
|
unsubscribe();
|
|
401
|
+
this.#eventWaiterRejectors.delete(closeHandler);
|
|
392
402
|
if (timeout) clearTimeout(timeout);
|
|
393
403
|
if (signal) {
|
|
394
404
|
signal.removeEventListener("abort", abortHandler);
|
|
@@ -398,6 +408,10 @@ export class DapClient {
|
|
|
398
408
|
cleanup();
|
|
399
409
|
reject(signal?.reason instanceof Error ? signal.reason : new ToolAbortError());
|
|
400
410
|
};
|
|
411
|
+
const closeHandler = (error: Error) => {
|
|
412
|
+
cleanup();
|
|
413
|
+
reject(error);
|
|
414
|
+
};
|
|
401
415
|
const unsubscribe = this.onEvent(event, body => {
|
|
402
416
|
const typedBody = body as TBody;
|
|
403
417
|
if (predicate && !predicate(typedBody)) {
|
|
@@ -406,6 +420,7 @@ export class DapClient {
|
|
|
406
420
|
cleanup();
|
|
407
421
|
resolve(typedBody);
|
|
408
422
|
});
|
|
423
|
+
this.#eventWaiterRejectors.add(closeHandler);
|
|
409
424
|
if (signal) {
|
|
410
425
|
signal.addEventListener("abort", abortHandler, { once: true });
|
|
411
426
|
}
|
|
@@ -580,6 +595,7 @@ export class DapClient {
|
|
|
580
595
|
|
|
581
596
|
const framer = new MessageFramer(this.#messageBuffer);
|
|
582
597
|
|
|
598
|
+
let closeError: Error | undefined;
|
|
583
599
|
try {
|
|
584
600
|
while (true) {
|
|
585
601
|
const { done, value } = await reader.read();
|
|
@@ -619,13 +635,19 @@ export class DapClient {
|
|
|
619
635
|
}
|
|
620
636
|
}
|
|
621
637
|
} catch (error) {
|
|
622
|
-
|
|
638
|
+
closeError = new Error(`DAP connection closed: ${toErrorMessage(error)}`);
|
|
623
639
|
} finally {
|
|
624
640
|
// Persist any unparsed remainder so a restarted reader resumes mid-message.
|
|
625
641
|
this.#messageBuffer = framer.remainder();
|
|
626
642
|
reader.releaseLock();
|
|
627
643
|
this.#isReading = false;
|
|
628
644
|
}
|
|
645
|
+
// The transport is gone once the reader loop exits — on a thrown error
|
|
646
|
+
// or a clean stream end (a socket the peer dropped after we wrote, e.g.
|
|
647
|
+
// the WSL2-mirrored ghost-accept race in issue #6055). Fail every
|
|
648
|
+
// in-flight request and event waiter so callers see an immediate error
|
|
649
|
+
// instead of waiting out their own timeout.
|
|
650
|
+
this.#failConnection(closeError ?? new Error(`DAP connection closed: ${this.adapter.name} transport ended`));
|
|
629
651
|
}
|
|
630
652
|
|
|
631
653
|
#handleResponse(message: DapResponseMessage): void {
|
|
@@ -712,7 +734,19 @@ export class DapClient {
|
|
|
712
734
|
? `DAP adapter exited (code ${exitCode}): ${stderr}`
|
|
713
735
|
: `DAP adapter exited unexpectedly (code ${exitCode})`,
|
|
714
736
|
);
|
|
737
|
+
this.#failConnection(error);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Reject every in-flight request and wake every event waiter with `error`.
|
|
741
|
+
* Called when the transport dies (reader end, socket close, adapter exit)
|
|
742
|
+
* so nothing sits pending until its own timeout. */
|
|
743
|
+
#failConnection(error: Error): void {
|
|
715
744
|
this.#rejectPendingRequests(error);
|
|
745
|
+
const waiters = Array.from(this.#eventWaiterRejectors);
|
|
746
|
+
this.#eventWaiterRejectors.clear();
|
|
747
|
+
for (const reject of waiters) {
|
|
748
|
+
reject(error);
|
|
749
|
+
}
|
|
716
750
|
}
|
|
717
751
|
|
|
718
752
|
#rejectPendingRequests(error: Error): void {
|
|
@@ -826,6 +860,51 @@ async function waitForTcpTransport(
|
|
|
826
860
|
throw new Error(`TCP port ${host}:${port} was not ready after ${timeoutMs}ms`);
|
|
827
861
|
}
|
|
828
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Give the adapter a chance to announce it is listening on `port` before the
|
|
865
|
+
* first connect. vscode-js-debug prints `Debug server listening at HOST:PORT`
|
|
866
|
+
* to stdout from inside its `listen()` callback; waiting for the port to appear
|
|
867
|
+
* there means we only connect once the child genuinely owns the reserved port,
|
|
868
|
+
* which closes the WSL2-mirrored ghost-accept window (issue #6055) at its root.
|
|
869
|
+
*
|
|
870
|
+
* Best-effort: resolves on the banner, on process exit, or on timeout — the
|
|
871
|
+
* subsequent connect loop and `proc.exitCode` checks surface real failures, so
|
|
872
|
+
* an adapter that never prints a banner still proceeds (just without the gate).
|
|
873
|
+
* Also drains stdout for the wait's duration: in tcp mode the DAP protocol
|
|
874
|
+
* flows over the socket, so nothing else consumes the adapter's stdout.
|
|
875
|
+
*
|
|
876
|
+
* Exported so tests can drive the gate deterministically with a synthetic stdout.
|
|
877
|
+
*/
|
|
878
|
+
export async function waitForTcpServerListening(
|
|
879
|
+
proc: { stdout: ReadableStream<Uint8Array>; exitCode: number | null },
|
|
880
|
+
port: number,
|
|
881
|
+
timeoutMs: number,
|
|
882
|
+
): Promise<void> {
|
|
883
|
+
const ready = Promise.withResolvers<void>();
|
|
884
|
+
const portText = String(port);
|
|
885
|
+
void (async () => {
|
|
886
|
+
try {
|
|
887
|
+
const decoder = new TextDecoder();
|
|
888
|
+
let buffered = "";
|
|
889
|
+
for await (const chunk of proc.stdout) {
|
|
890
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
891
|
+
if (buffered.includes(portText)) {
|
|
892
|
+
ready.resolve();
|
|
893
|
+
}
|
|
894
|
+
// Keep only the tail relevant for banner matching so a chatty
|
|
895
|
+
// adapter cannot grow this buffer without bound.
|
|
896
|
+
if (buffered.length > 4096) {
|
|
897
|
+
buffered = buffered.slice(-1024);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
} catch {
|
|
901
|
+
/* stdout errored — the connect loop surfaces the real failure */
|
|
902
|
+
}
|
|
903
|
+
ready.resolve();
|
|
904
|
+
})();
|
|
905
|
+
await Promise.race([ready.promise, Bun.sleep(timeoutMs)]);
|
|
906
|
+
}
|
|
907
|
+
|
|
829
908
|
interface SocketTransport {
|
|
830
909
|
readable: ReadableStream<Uint8Array>;
|
|
831
910
|
writeSink: DapWriteSink;
|
package/src/edit/diff.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides diff string generation and the replace-mode edit logic
|
|
5
5
|
* used when not in patch mode.
|
|
6
6
|
*/
|
|
7
|
-
import
|
|
7
|
+
import { diffLines, structuredPatchHunks } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { resolveToCwd } from "../tools/path-utils";
|
|
9
9
|
import { type BlockContextSource, findBlockContextLines } from "../utils/block-context";
|
|
10
10
|
import { DEFAULT_FUZZY_THRESHOLD, EditMatchError, findMatch } from "./modes/replace";
|
|
@@ -235,7 +235,7 @@ export function generateDiffString(
|
|
|
235
235
|
contextLines = 2,
|
|
236
236
|
source: BlockContextSource = {},
|
|
237
237
|
): DiffResult {
|
|
238
|
-
const parts =
|
|
238
|
+
const parts = diffLines(oldContent, newContent);
|
|
239
239
|
const output: string[] = [];
|
|
240
240
|
|
|
241
241
|
let oldLineNum = 1;
|
|
@@ -373,10 +373,10 @@ export function generateUnifiedDiffString(
|
|
|
373
373
|
contextLines = 3,
|
|
374
374
|
source: BlockContextSource = {},
|
|
375
375
|
): DiffResult {
|
|
376
|
-
const
|
|
376
|
+
const hunks = structuredPatchHunks(oldContent, newContent, contextLines);
|
|
377
377
|
const output: string[] = [];
|
|
378
378
|
let firstChangedLine: number | undefined;
|
|
379
|
-
for (const hunk of
|
|
379
|
+
for (const hunk of hunks) {
|
|
380
380
|
output.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
381
381
|
let oldLine = hunk.oldStart;
|
|
382
382
|
let newLine = hunk.newStart;
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -153,7 +153,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
153
153
|
"""Read task/agent output by ID. Returns text or JSON depending on format.
|
|
154
154
|
|
|
155
155
|
Args:
|
|
156
|
-
*ids: Output IDs to read (e.g., '
|
|
156
|
+
*ids: Output IDs to read (e.g., 'scout_0', 'reviewer_1')
|
|
157
157
|
format: 'raw' (default), 'json' (dict with metadata), 'stripped' (no ANSI)
|
|
158
158
|
query: jq-like query for JSON outputs (e.g., '.endpoints[0].file')
|
|
159
159
|
offset: Line number to start reading from (1-indexed)
|
|
@@ -164,11 +164,11 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
164
164
|
Multiple IDs: list of dict with 'id' and 'content'/'data' keys
|
|
165
165
|
|
|
166
166
|
Examples:
|
|
167
|
-
output('
|
|
167
|
+
output('scout_0') # Read as raw text
|
|
168
168
|
output('reviewer_0', format='json') # Read with metadata
|
|
169
|
-
output('
|
|
170
|
-
output('
|
|
171
|
-
output('
|
|
169
|
+
output('scout_0', query='.files[0]') # Extract JSON field
|
|
170
|
+
output('scout_0', offset=10, limit=20) # Lines 10-29
|
|
171
|
+
output('scout_0', 'reviewer_1') # Read multiple outputs
|
|
172
172
|
"""
|
|
173
173
|
# Prefer PI_ARTIFACTS_DIR so subagents resolve through the parent's
|
|
174
174
|
# shared artifacts dir; fall back to deriving from PI_SESSION_FILE
|