@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3
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 +162 -40
- package/dist/cli.js +4559 -4543
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
package/src/tools/read.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { normalizeToLF } from "../edit/normalize";
|
|
37
37
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
38
38
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
39
|
-
import { InternalUrlRouter, resolveLocalUrlToFile } from "../internal-urls";
|
|
39
|
+
import { InternalUrlRouter, resolveLocalUrlToFile, resolveLocalUrlToPath } from "../internal-urls";
|
|
40
40
|
import { type ResolvedArtifactFile, resolveArtifactFile } from "../internal-urls/artifact-protocol";
|
|
41
41
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
42
42
|
import type { InternalUrl } from "../internal-urls/types";
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
MAX_IMAGE_INPUT_BYTES,
|
|
63
63
|
webpExclusionForModel,
|
|
64
64
|
} from "../utils/image-loading";
|
|
65
|
-
import { convertFileWithMarkit } from "../utils/markit";
|
|
65
|
+
import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
|
|
66
66
|
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
67
67
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
68
68
|
import {
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
} from "./conflict-detect";
|
|
79
79
|
import {
|
|
80
80
|
executeReadUrl,
|
|
81
|
-
|
|
81
|
+
fetchReadUrl,
|
|
82
82
|
parseReadUrlTarget,
|
|
83
83
|
type ReadUrlToolDetails,
|
|
84
84
|
renderReadUrlCall,
|
|
@@ -151,9 +151,6 @@ function getSummaryParseCache(session: object): LRUCache<string, SummaryResult |
|
|
|
151
151
|
return cache;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Document types converted to markdown via markit.
|
|
155
|
-
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
156
|
-
|
|
157
154
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
158
155
|
const MAX_SUMMARY_LINES = 20_000;
|
|
159
156
|
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
@@ -920,6 +917,32 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
920
917
|
});
|
|
921
918
|
}
|
|
922
919
|
|
|
920
|
+
/**
|
|
921
|
+
* Recover the active approved plan when a model rewrites its `local://` URL
|
|
922
|
+
* as a same-basename path in the working-directory root.
|
|
923
|
+
*
|
|
924
|
+
* Only missing cwd-root paths qualify, so a real working-tree file always
|
|
925
|
+
* wins and unrelated paths cannot escape into the session artifact sandbox.
|
|
926
|
+
*/
|
|
927
|
+
#approvedPlanAlias(missingAbsolutePath: string): string | undefined {
|
|
928
|
+
const planReferencePath = this.session.getPlanReferencePath?.();
|
|
929
|
+
if (!planReferencePath?.startsWith("local:")) return undefined;
|
|
930
|
+
|
|
931
|
+
const requestedPath = path.resolve(missingAbsolutePath);
|
|
932
|
+
if (path.dirname(requestedPath) !== path.resolve(this.session.cwd)) return undefined;
|
|
933
|
+
|
|
934
|
+
const localProtocolOptions = this.session.localProtocolOptions ?? {
|
|
935
|
+
getArtifactsDir: () => this.session.getArtifactsDir?.() ?? null,
|
|
936
|
+
getSessionId: () => this.session.getSessionId?.() ?? null,
|
|
937
|
+
};
|
|
938
|
+
try {
|
|
939
|
+
const approvedPlanPath = resolveLocalUrlToPath(planReferencePath, localProtocolOptions);
|
|
940
|
+
return path.basename(requestedPath) === path.basename(approvedPlanPath) ? approvedPlanPath : undefined;
|
|
941
|
+
} catch {
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
923
946
|
async #tryReadDelimitedPaths(
|
|
924
947
|
readPath: string,
|
|
925
948
|
signal?: AbortSignal,
|
|
@@ -2141,15 +2164,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2141
2164
|
const urlRaw = parsedUrlTarget.raw;
|
|
2142
2165
|
const urlRanges = parsedUrlTarget.ranges;
|
|
2143
2166
|
if (urlRanges !== undefined && urlRanges.length > 1) {
|
|
2144
|
-
const
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
return this.#buildInMemoryMultiRangeResult(cached.output, urlRanges, {
|
|
2151
|
-
details: { ...cached.details },
|
|
2152
|
-
sourceUrl: cached.details.finalUrl,
|
|
2167
|
+
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2168
|
+
ensureArtifact: true,
|
|
2169
|
+
});
|
|
2170
|
+
return this.#buildInMemoryMultiRangeResult(entry.output, urlRanges, {
|
|
2171
|
+
details: { ...entry.details },
|
|
2172
|
+
sourceUrl: entry.details.finalUrl,
|
|
2153
2173
|
entityLabel: "URL output",
|
|
2154
2174
|
raw: urlRaw,
|
|
2155
2175
|
immutable: true,
|
|
@@ -2158,18 +2178,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2158
2178
|
const urlOffset = parsedUrlTarget.offset;
|
|
2159
2179
|
const urlLimit = parsedUrlTarget.limit;
|
|
2160
2180
|
if (urlOffset !== undefined || urlLimit !== undefined) {
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
{
|
|
2166
|
-
|
|
2167
|
-
preferCached: true,
|
|
2168
|
-
},
|
|
2169
|
-
);
|
|
2170
|
-
return this.#buildInMemoryTextResult(cached.output, urlOffset, urlLimit, {
|
|
2171
|
-
details: { ...cached.details },
|
|
2172
|
-
sourceUrl: cached.details.finalUrl,
|
|
2181
|
+
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2182
|
+
ensureArtifact: true,
|
|
2183
|
+
});
|
|
2184
|
+
return this.#buildInMemoryTextResult(entry.output, urlOffset, urlLimit, {
|
|
2185
|
+
details: { ...entry.details },
|
|
2186
|
+
sourceUrl: entry.details.finalUrl,
|
|
2173
2187
|
entityLabel: "URL output",
|
|
2174
2188
|
raw: urlRaw,
|
|
2175
2189
|
immutable: true,
|
|
@@ -2292,7 +2306,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2292
2306
|
isDirectory = stat.isDirectory();
|
|
2293
2307
|
} catch (error) {
|
|
2294
2308
|
if (isNotFoundError(error)) {
|
|
2295
|
-
// Attempt unique suffix resolution before falling back to
|
|
2309
|
+
// Attempt unique suffix resolution before falling back to the approved-plan
|
|
2310
|
+
// alias or fuzzy suggestions. Existing workspace files retain precedence.
|
|
2296
2311
|
if (!isRemoteMountPath(absolutePath)) {
|
|
2297
2312
|
const suffixMatch = await this.#findSuffixMatchCached(suffixCache, localReadPath, signal);
|
|
2298
2313
|
if (suffixMatch) {
|
|
@@ -2303,12 +2318,30 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2303
2318
|
isDirectory = retryStat.isDirectory();
|
|
2304
2319
|
suffixResolution = { from: localReadPath, to: suffixMatch.displayPath };
|
|
2305
2320
|
} catch {
|
|
2306
|
-
// Suffix match candidate no longer stats —
|
|
2321
|
+
// Suffix match candidate no longer stats — continue through
|
|
2322
|
+
// approved-plan recovery and the original not-found error.
|
|
2307
2323
|
}
|
|
2308
2324
|
}
|
|
2309
2325
|
}
|
|
2310
2326
|
|
|
2327
|
+
let recoveredApprovedPlan = false;
|
|
2311
2328
|
if (!suffixResolution) {
|
|
2329
|
+
const approvedPlanPath = this.#approvedPlanAlias(absolutePath);
|
|
2330
|
+
if (approvedPlanPath) {
|
|
2331
|
+
try {
|
|
2332
|
+
const approvedPlanStat = await Bun.file(approvedPlanPath).stat();
|
|
2333
|
+
absolutePath = approvedPlanPath;
|
|
2334
|
+
fileSize = approvedPlanStat.size;
|
|
2335
|
+
isDirectory = approvedPlanStat.isDirectory();
|
|
2336
|
+
recoveredApprovedPlan = true;
|
|
2337
|
+
} catch {
|
|
2338
|
+
// The referenced plan disappeared after resolution; continue through
|
|
2339
|
+
// the ordinary delimited-path fallback and not-found error.
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
if (!recoveredApprovedPlan && !suffixResolution) {
|
|
2312
2345
|
const delimitedResult = await this.#tryReadDelimitedPaths(readPath, signal);
|
|
2313
2346
|
if (delimitedResult) return delimitedResult;
|
|
2314
2347
|
throw new ToolError(`Path '${localReadPath}' not found`);
|
package/src/tools/todo.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { ToolSession } from "../sdk";
|
|
|
12
12
|
import type { SessionEntry } from "../session/session-entries";
|
|
13
13
|
import { framedBlock, renderStatusLine, renderTreeList } from "../tui";
|
|
14
14
|
import { normalizePathLikeInput, resolveToCwd } from "./path-utils";
|
|
15
|
-
import { formatErrorDetail, PREVIEW_LIMITS } from "./render-utils";
|
|
15
|
+
import { formatErrorDetail, formatMoreItems, PREVIEW_LIMITS, pluralize } from "./render-utils";
|
|
16
16
|
|
|
17
17
|
// =============================================================================
|
|
18
18
|
// Types
|
|
@@ -214,6 +214,80 @@ export function todoMatchesAnyDescription(content: string, descriptions: readonl
|
|
|
214
214
|
return false;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* A todo the collapsed viewport treats as current work: the literal
|
|
219
|
+
* `in_progress` task or a pending task a live subagent is executing. Both
|
|
220
|
+
* collapsed views (transient tool result + sticky HUD) run this same policy so
|
|
221
|
+
* they can never disagree about what the agent is doing (#5873).
|
|
222
|
+
*/
|
|
223
|
+
function isActiveTodo<T extends { status: TodoStatus }>(task: T, isMatched: (task: T) => boolean): boolean {
|
|
224
|
+
return task.status === "in_progress" || (task.status === "pending" && isMatched(task));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Result of {@link selectCollapsedTodos}: the rows to render plus an optional
|
|
228
|
+
* summary line (empty string ⇒ no summary row). */
|
|
229
|
+
export interface CollapsedTodoSelection<T> {
|
|
230
|
+
items: T[];
|
|
231
|
+
summary: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Walking-viewport selection for a phase's collapsed todo preview (#5873).
|
|
236
|
+
*
|
|
237
|
+
* Policy, applied to `tasks` in todo order:
|
|
238
|
+
* 1. While the phase has open work, completed/abandoned tasks are omitted. A
|
|
239
|
+
* phase with no open tasks left falls back to its closed tasks so the sticky
|
|
240
|
+
* HUD's closed-todo persistence still has something to render.
|
|
241
|
+
* 2. Every active task (in-progress, or pending matched to a live subagent) is
|
|
242
|
+
* placed at the head in stable todo order — never dropped for lying outside
|
|
243
|
+
* an ordinary window.
|
|
244
|
+
* 3. Remaining rows up to `cap` are filled with the pending tasks that follow
|
|
245
|
+
* the first active one, in todo order (falling back to leading pending tasks
|
|
246
|
+
* when no active task exists), so a freshly-promoted task leads the preview.
|
|
247
|
+
* 4. When active tasks alone exceed `cap`, only the first `cap` active tasks are
|
|
248
|
+
* shown and the summary counts the hidden *active* todos, never replacing
|
|
249
|
+
* them with unrelated pending rows.
|
|
250
|
+
*
|
|
251
|
+
* The summary otherwise counts the remaining tasks in the display base. Returns
|
|
252
|
+
* the whole base with an empty summary when it already fits.
|
|
253
|
+
*/
|
|
254
|
+
export function selectCollapsedTodos<T extends { status: TodoStatus }>(
|
|
255
|
+
tasks: T[],
|
|
256
|
+
isMatched: (task: T) => boolean,
|
|
257
|
+
cap: number,
|
|
258
|
+
): CollapsedTodoSelection<T> {
|
|
259
|
+
const open = tasks.filter(task => task.status === "pending" || task.status === "in_progress");
|
|
260
|
+
// No open work: fall back to the closed tasks so a settled phase still
|
|
261
|
+
// renders (HUD closed-todo persistence). Closed tasks are never active.
|
|
262
|
+
const base = open.length > 0 ? open : tasks;
|
|
263
|
+
if (base.length <= cap) return { items: base, summary: "" };
|
|
264
|
+
|
|
265
|
+
const active = base.filter(task => isActiveTodo(task, isMatched));
|
|
266
|
+
// Only when active work strictly exceeds the cap do we drop pending rows and
|
|
267
|
+
// count hidden *actives*. At exactly `cap` actives, fall through so the normal
|
|
268
|
+
// branch still surfaces any following pending work in the summary.
|
|
269
|
+
if (active.length > cap) {
|
|
270
|
+
const hiddenActive = active.length - cap;
|
|
271
|
+
return {
|
|
272
|
+
items: active.slice(0, cap),
|
|
273
|
+
summary: `… ${hiddenActive} more active ${pluralize("todo", hiddenActive)}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Fill trailing rows with tasks following the first active one, so the
|
|
278
|
+
// promoted/current task leads and its successors follow in todo order.
|
|
279
|
+
const firstActiveIdx = active.length > 0 ? base.indexOf(active[0]) : 0;
|
|
280
|
+
const fill: T[] = [];
|
|
281
|
+
for (let i = firstActiveIdx; i < base.length && active.length + fill.length < cap; i++) {
|
|
282
|
+
const task = base[i];
|
|
283
|
+
if (isActiveTodo(task, isMatched)) continue;
|
|
284
|
+
fill.push(task);
|
|
285
|
+
}
|
|
286
|
+
const items = [...active, ...fill];
|
|
287
|
+
const hidden = base.length - items.length;
|
|
288
|
+
return { items, summary: hidden > 0 ? formatMoreItems(hidden, "todo") : "" };
|
|
289
|
+
}
|
|
290
|
+
|
|
217
291
|
function resolveTaskOrError(
|
|
218
292
|
phases: TodoPhase[],
|
|
219
293
|
content: string | undefined,
|
|
@@ -755,6 +829,7 @@ function formatTodoLine(
|
|
|
755
829
|
prefix: string,
|
|
756
830
|
completionKeys: Set<string>,
|
|
757
831
|
frame: number | undefined,
|
|
832
|
+
matched = false,
|
|
758
833
|
): string {
|
|
759
834
|
const checkbox = uiTheme.checkbox;
|
|
760
835
|
switch (item.status) {
|
|
@@ -771,7 +846,9 @@ function formatTodoLine(
|
|
|
771
846
|
case "abandoned":
|
|
772
847
|
return uiTheme.fg("error", `${prefix}${checkbox.unchecked} ${strikethroughText(item.content)}`);
|
|
773
848
|
default:
|
|
774
|
-
|
|
849
|
+
// A pending todo lit by a live subagent match renders accent, matching
|
|
850
|
+
// the sticky HUD's convention (#5873).
|
|
851
|
+
return uiTheme.fg(matched ? "accent" : "dim", `${prefix}${checkbox.unchecked} ${item.content}`);
|
|
775
852
|
}
|
|
776
853
|
}
|
|
777
854
|
|
|
@@ -822,6 +899,21 @@ function formatPhaseSummary(phase: TodoPhase, oneBasedIndex: number, uiTheme: Th
|
|
|
822
899
|
return `${name}${uiTheme.fg("dim", ` ${done}/${total}`)}`;
|
|
823
900
|
}
|
|
824
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Live subagent descriptions the transient tool result uses to detect
|
|
904
|
+
* pending todos being executed by an in-flight subagent, so its collapsed
|
|
905
|
+
* viewport surfaces the same active work the sticky HUD does (#5873). Wired
|
|
906
|
+
* once by interactive mode from its observer registry; returns `[]` outside an
|
|
907
|
+
* interactive session (tests, SDK, transcript rebuilds), where only literal
|
|
908
|
+
* `in_progress` counts as active.
|
|
909
|
+
*/
|
|
910
|
+
let activeTodoDescriptionsProvider: () => readonly string[] = () => [];
|
|
911
|
+
|
|
912
|
+
/** Wire the live-subagent description source for {@link todoToolRenderer}. */
|
|
913
|
+
export function setActiveTodoDescriptionsProvider(provider: () => readonly string[]): void {
|
|
914
|
+
activeTodoDescriptionsProvider = provider;
|
|
915
|
+
}
|
|
916
|
+
|
|
825
917
|
export const todoToolRenderer = {
|
|
826
918
|
renderCall(args: TodoRenderArgs, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
827
919
|
// `args` is the raw partially-parsed JSON from the streaming tool-call
|
|
@@ -903,6 +995,12 @@ export const todoToolRenderer = {
|
|
|
903
995
|
// a single task flip doesn't redraw every phase's full task list. The
|
|
904
996
|
// manual expand toggle (and the no-signal fallback) still shows all.
|
|
905
997
|
const touched = expanded || !multiPhase ? null : computeTouchedPhases(args, phases, completedTasks);
|
|
998
|
+
// A pending todo counts as active work when an in-flight subagent is
|
|
999
|
+
// executing it — the transient result surfaces the same active set the
|
|
1000
|
+
// sticky HUD does (#5873). Empty outside an interactive session.
|
|
1001
|
+
const activeDescs = expanded ? [] : activeTodoDescriptionsProvider();
|
|
1002
|
+
const isMatched = (task: TodoItem): boolean =>
|
|
1003
|
+
activeDescs.length > 0 && todoMatchesAnyDescription(task.content, activeDescs);
|
|
906
1004
|
const bodyLines: string[] = [];
|
|
907
1005
|
for (let p = 0; p < phases.length; p++) {
|
|
908
1006
|
const phase = phases[p];
|
|
@@ -914,17 +1012,32 @@ export const todoToolRenderer = {
|
|
|
914
1012
|
bodyLines.push(uiTheme.fg("accent", chalk.bold(formatPhaseDisplayName(phase.name, p + 1))));
|
|
915
1013
|
}
|
|
916
1014
|
const completionKeys = completionKeysByPhase.get(phase.name) ?? EMPTY_COMPLETION_KEYS;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1015
|
+
// Collapsed: walking viewport — completed/abandoned omitted, active
|
|
1016
|
+
// work (in-progress / subagent-matched) pulled to the head, then
|
|
1017
|
+
// following pending tasks (#5873). Expanded: every task in order.
|
|
1018
|
+
const treeLines = expanded
|
|
1019
|
+
? renderTreeList(
|
|
1020
|
+
{
|
|
1021
|
+
items: phase.tasks,
|
|
1022
|
+
expanded,
|
|
1023
|
+
itemType: "todo",
|
|
1024
|
+
renderItem: todo => formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame),
|
|
1025
|
+
},
|
|
1026
|
+
uiTheme,
|
|
1027
|
+
)
|
|
1028
|
+
: (() => {
|
|
1029
|
+
const selection = selectCollapsedTodos(phase.tasks, isMatched, PREVIEW_LIMITS.COLLAPSED_ITEMS);
|
|
1030
|
+
return renderTreeList(
|
|
1031
|
+
{
|
|
1032
|
+
items: selection.items,
|
|
1033
|
+
itemType: "todo",
|
|
1034
|
+
trailingSummary: selection.summary,
|
|
1035
|
+
renderItem: todo =>
|
|
1036
|
+
formatTodoLine(todo, uiTheme, "", completionKeys, spinnerFrame, isMatched(todo)),
|
|
1037
|
+
},
|
|
1038
|
+
uiTheme,
|
|
1039
|
+
);
|
|
1040
|
+
})();
|
|
928
1041
|
for (const line of treeLines) {
|
|
929
1042
|
bodyLines.push(`${indent}${line}`);
|
|
930
1043
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
writeArchive,
|
|
37
37
|
} from "../utils/zip";
|
|
38
38
|
import { routeWriteThroughBridge } from "./acp-bridge";
|
|
39
|
-
import { truncateForPrompt } from "./approval";
|
|
39
|
+
import { resolveToolTier, truncateForPrompt } from "./approval";
|
|
40
40
|
import { assertEditableFile } from "./auto-generated-guard";
|
|
41
41
|
import {
|
|
42
42
|
type ConflictEntry,
|
|
@@ -398,9 +398,27 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
398
398
|
if (xdevTarget.name === REPORT_ISSUE_DEVICE_NAME) return "write";
|
|
399
399
|
if (xdevTarget.name && isResolutionDeviceName(xdevTarget.name)) return "read";
|
|
400
400
|
const inst = xdevTarget.name ? this.session.xdevRegistry?.get(xdevTarget.name) : undefined;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
401
|
+
if (!inst) return "exec";
|
|
402
|
+
// Decode the device JSON payload and evaluate the mounted tool's own
|
|
403
|
+
// approval (which may be argument-dependent, e.g. ast_edit is read-tier
|
|
404
|
+
// for internal-URL paths, debug is read-tier for inspection actions).
|
|
405
|
+
// Malformed JSON, non-object payloads, missing content, and approval
|
|
406
|
+
// functions that reject schema-invalid objects stay exec so the gate
|
|
407
|
+
// fails closed — the dispatch itself rejects invalid arguments too.
|
|
408
|
+
const rawContent = (args as Partial<WriteParams>).content;
|
|
409
|
+
if (typeof rawContent !== "string") return "exec";
|
|
410
|
+
let parsed: unknown;
|
|
411
|
+
try {
|
|
412
|
+
parsed = JSON.parse(rawContent);
|
|
413
|
+
} catch {
|
|
414
|
+
return "exec";
|
|
415
|
+
}
|
|
416
|
+
if (!isRecord(parsed)) return "exec";
|
|
417
|
+
try {
|
|
418
|
+
return resolveToolTier(inst, parsed);
|
|
419
|
+
} catch {
|
|
420
|
+
return "exec";
|
|
421
|
+
}
|
|
404
422
|
}
|
|
405
423
|
// Remote SSH writes open an outbound connection and run a remote shell —
|
|
406
424
|
// gate them like the exec-tier `ssh` tool, ahead of the handler-write
|
package/src/tools/xdev.ts
CHANGED
|
@@ -44,15 +44,26 @@ import { ToolError } from "./tool-errors";
|
|
|
44
44
|
*/
|
|
45
45
|
export const XDEV_KEEP_TOP_LEVEL: Record<string, true> = { todo: true, ask: true, grep: true };
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Tools that carry the `xd://` transport itself and therefore can never be
|
|
49
|
+
* mounted as devices: `read xd://` lists/documents devices and
|
|
50
|
+
* `write xd://<tool>` executes them. Demoting either leaves every mounted
|
|
51
|
+
* device unreachable (issue #5764), so they stay top-level regardless of a
|
|
52
|
+
* declared `loadMode`.
|
|
53
|
+
*/
|
|
54
|
+
export const XDEV_TRANSPORT_TOOLS: Record<string, true> = { read: true, write: true };
|
|
55
|
+
|
|
47
56
|
/**
|
|
48
57
|
* Whether an enabled tool is presented under `xd://` (rather than top-level)
|
|
49
58
|
* while the `xd://` transport is active. Discoverable tools mount unless they
|
|
50
|
-
* are pinned top-level by {@link XDEV_KEEP_TOP_LEVEL}
|
|
51
|
-
*
|
|
59
|
+
* are pinned top-level by {@link XDEV_KEEP_TOP_LEVEL} or carry the transport
|
|
60
|
+
* itself ({@link XDEV_TRANSPORT_TOOLS}); essential tools never do. The caller
|
|
61
|
+
* gates this on the transport being active (a session-owned
|
|
52
62
|
* {@link XdevRegistry} existing).
|
|
53
63
|
*/
|
|
54
64
|
export function isMountableUnderXdev(tool: { name: string; loadMode?: ToolLoadMode }): boolean {
|
|
55
|
-
|
|
65
|
+
if (tool.name in XDEV_TRANSPORT_TOOLS || tool.name in XDEV_KEEP_TOP_LEVEL) return false;
|
|
66
|
+
return tool.loadMode === "discoverable";
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/** Dispatch metadata carried on write-tool details for renderer delegation. */
|
package/src/tui/tree-list.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface TreeListOptions<T> {
|
|
|
18
18
|
maxCollapsedLines?: number;
|
|
19
19
|
itemType?: string;
|
|
20
20
|
truncateFrom?: "start" | "end";
|
|
21
|
+
/** Caller-supplied trailing summary line. When set (and not expanded),
|
|
22
|
+
* `renderTreeList` renders exactly the provided `items` (the caller has
|
|
23
|
+
* already applied its own selection/cap) and appends this text as the
|
|
24
|
+
* final `└` row, with the last item using `├`. Empty string renders the
|
|
25
|
+
* items with no summary. Bypasses the built-in truncation/`maxCollapsed`
|
|
26
|
+
* path. */
|
|
27
|
+
trailingSummary?: string;
|
|
21
28
|
/** Called once per item with `isLast: false` during budget calculation;
|
|
22
29
|
* line count MUST NOT vary based on `isLast`. */
|
|
23
30
|
renderItem: (item: T, context: TreeContext) => string | string[];
|
|
@@ -36,6 +43,38 @@ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): st
|
|
|
36
43
|
const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
|
|
37
44
|
const linesBudget = !expanded && maxCollapsedLines !== undefined ? maxCollapsedLines : Infinity;
|
|
38
45
|
|
|
46
|
+
// Caller-driven collapse: render exactly the provided items (the caller
|
|
47
|
+
// already picked/capped them) plus an optional trailing summary row. The
|
|
48
|
+
// walking-viewport todo policy uses this so item selection lives in the
|
|
49
|
+
// todo domain, not here.
|
|
50
|
+
if (!expanded && options.trailingSummary !== undefined) {
|
|
51
|
+
const summary = options.trailingSummary;
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
for (let i = 0; i < items.length; i++) {
|
|
54
|
+
const rendered = renderItem(items[i], {
|
|
55
|
+
index: i,
|
|
56
|
+
isLast: false,
|
|
57
|
+
depth: 0,
|
|
58
|
+
theme,
|
|
59
|
+
prefix: "",
|
|
60
|
+
continuePrefix: "",
|
|
61
|
+
});
|
|
62
|
+
const itemLines = Array.isArray(rendered) ? rendered : rendered ? [rendered] : [];
|
|
63
|
+
if (itemLines.length === 0) continue;
|
|
64
|
+
const isLast = summary === "" && i === items.length - 1;
|
|
65
|
+
const prefix = `${theme.fg("dim", getTreeBranch(isLast, theme))} `;
|
|
66
|
+
const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
|
|
67
|
+
lines.push(`${prefix}${replaceTabs(itemLines[0]!)}`);
|
|
68
|
+
for (let j = 1; j < itemLines.length; j++) {
|
|
69
|
+
lines.push(`${continuePrefix}${replaceTabs(itemLines[j]!)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (summary !== "") {
|
|
73
|
+
lines.push(`${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", summary)}`);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
const candidateIndices: number[] = [];
|
|
40
79
|
if (truncateFrom === "start") {
|
|
41
80
|
const startCandidateIdx = Math.max(0, items.length - maxItems);
|
package/src/utils/markit.ts
CHANGED
|
@@ -10,6 +10,18 @@ import {
|
|
|
10
10
|
} from "./markit-cache";
|
|
11
11
|
import { loadEmbeddedMupdfWasm } from "./mupdf-wasm-embed";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* File extensions markit can actually convert to markdown — one per registered
|
|
15
|
+
* converter in `src/markit/registry.ts` (pdf, docx, pptx, xlsx, epub). This is
|
|
16
|
+
* the single source of truth shared by the read, fetch, and CLI file tools so
|
|
17
|
+
* the advertised set never drifts from the converters that back it. Legacy
|
|
18
|
+
* binary formats (`.doc`, `.ppt`, `.xls`, `.rtf`) are intentionally absent:
|
|
19
|
+
* markit has no converter for them, so routing them here only produced an
|
|
20
|
+
* `Unsupported format` error instead of letting them fall through to the
|
|
21
|
+
* binary-file handling.
|
|
22
|
+
*/
|
|
23
|
+
export const CONVERTIBLE_EXTENSIONS: ReadonlySet<string> = new Set([".pdf", ".docx", ".pptx", ".xlsx", ".epub"]);
|
|
24
|
+
|
|
13
25
|
export interface MarkitConversionResult {
|
|
14
26
|
content: string;
|
|
15
27
|
ok: boolean;
|
|
@@ -68,6 +68,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
|
|
|
68
68
|
* resolver instead of a pre-evaluated value ensures the metadata's account_uuid
|
|
69
69
|
* reflects the credential actually selected for this request.
|
|
70
70
|
* @param customSystemPrompt Optional title-specific system prompt override
|
|
71
|
+
* @param signal Session-lifecycle cancellation for background title requests
|
|
71
72
|
*/
|
|
72
73
|
export async function generateSessionTitle(
|
|
73
74
|
firstMessage: string,
|
|
@@ -77,6 +78,7 @@ export async function generateSessionTitle(
|
|
|
77
78
|
currentModel?: Model<Api>,
|
|
78
79
|
metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
|
|
79
80
|
customSystemPrompt?: string,
|
|
81
|
+
signal?: AbortSignal,
|
|
80
82
|
): Promise<string | null> {
|
|
81
83
|
// Defer titling for greetings / acknowledgements / empty input. The default
|
|
82
84
|
// tiny title model can't reliably decline trivial input, so this happens
|
|
@@ -97,7 +99,7 @@ export async function generateSessionTitle(
|
|
|
97
99
|
sessionId,
|
|
98
100
|
currentModel,
|
|
99
101
|
metadataResolver,
|
|
100
|
-
|
|
102
|
+
signal,
|
|
101
103
|
titleSystemPrompt,
|
|
102
104
|
);
|
|
103
105
|
}
|
|
@@ -117,9 +119,18 @@ export async function generateSessionTitle(
|
|
|
117
119
|
return null;
|
|
118
120
|
}
|
|
119
121
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
let localTitle: string | null;
|
|
123
|
+
if (signal) {
|
|
124
|
+
localTitle = await tinyTitleClient.generate(
|
|
125
|
+
tinyModel,
|
|
126
|
+
firstMessage,
|
|
127
|
+
titleSystemPrompt ? { signal, systemPrompt: titleSystemPrompt } : { signal },
|
|
128
|
+
);
|
|
129
|
+
} else if (titleSystemPrompt) {
|
|
130
|
+
localTitle = await tinyTitleClient.generate(tinyModel, firstMessage, { systemPrompt: titleSystemPrompt });
|
|
131
|
+
} else {
|
|
132
|
+
localTitle = await tinyTitleClient.generate(tinyModel, firstMessage);
|
|
133
|
+
}
|
|
123
134
|
if (!localTitle) {
|
|
124
135
|
logger.warn("title-generator: local tiny model produced no title; skipping (no online fallback)", {
|
|
125
136
|
sessionId,
|
package/src/utils/zip.ts
CHANGED
|
@@ -233,12 +233,27 @@ function ensureParentDirectories(map: Map<string, ArchiveIndexEntry>): void {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Extensions that are ZIP containers under a different name — JVM (`.jar`,
|
|
238
|
+
* `.war`, `.ear`) and Android (`.apk`) packages are all ZIP archives. Treated
|
|
239
|
+
* as `zip` for member read/list and whole-archive rewrite.
|
|
240
|
+
*/
|
|
241
|
+
const ZIP_ALIAS_EXTENSIONS = ["jar", "war", "ear", "apk"] as const;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Regex alternation of every recognized archive extension, longest first so
|
|
245
|
+
* `.tar.gz` wins over `.tar`. Shared with `parseArchivePathCandidates` as its
|
|
246
|
+
* split pattern so extension recognition and path splitting never drift.
|
|
247
|
+
*/
|
|
248
|
+
const ARCHIVE_EXTENSION_ALTERNATION = ["tar\\.gz", "tgz", "zip", "tar", ...ZIP_ALIAS_EXTENSIONS].join("|");
|
|
249
|
+
|
|
236
250
|
/** Infer an archive format from a filesystem path's extension. */
|
|
237
251
|
export function archiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
|
|
238
252
|
const normalized = filePath.toLowerCase();
|
|
239
253
|
if (normalized.endsWith(".tar.gz") || normalized.endsWith(".tgz")) return "tar.gz";
|
|
240
254
|
if (normalized.endsWith(".tar")) return "tar";
|
|
241
255
|
if (normalized.endsWith(".zip")) return "zip";
|
|
256
|
+
if (ZIP_ALIAS_EXTENSIONS.some(ext => normalized.endsWith(`.${ext}`))) return "zip";
|
|
242
257
|
return undefined;
|
|
243
258
|
}
|
|
244
259
|
|
|
@@ -635,7 +650,7 @@ async function readZipEntries(source: ByteSource): Promise<ArchiveIndexEntry[]>
|
|
|
635
650
|
*/
|
|
636
651
|
export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] {
|
|
637
652
|
const normalized = filePath.replace(/\\/g, "/");
|
|
638
|
-
const pattern =
|
|
653
|
+
const pattern = new RegExp(`\\.(?:${ARCHIVE_EXTENSION_ALTERNATION})(?=(?::|$))`, "gi");
|
|
639
654
|
const seen = new Set<string>();
|
|
640
655
|
const candidates: ArchivePathCandidate[] = [];
|
|
641
656
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Kimi Web Search Provider
|
|
3
3
|
*
|
|
4
|
-
* Uses
|
|
4
|
+
* Uses the Kimi Code search API to retrieve web results. This is the Kimi Code
|
|
5
|
+
* membership service, distinct from the Moonshot Open Platform — it requires a
|
|
6
|
+
* Kimi Code Console credential (`omp /login kimi-code` or an explicit
|
|
7
|
+
* `MOONSHOT_SEARCH_API_KEY` / `KIMI_SEARCH_API_KEY`), not `MOONSHOT_API_KEY`.
|
|
5
8
|
* Endpoint: POST https://api.kimi.com/coding/v1/search
|
|
6
9
|
*/
|
|
7
10
|
import { type ApiKey, type AuthStorage, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
|
|
@@ -58,11 +61,17 @@ function resolveBaseUrl(): string {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
|
-
* Resolve the Kimi search credential. Highest precedence is the
|
|
62
|
-
* otherwise an AuthStorage-backed resolver for
|
|
63
|
-
*
|
|
64
|
-
* the central force-refresh / sibling-rotate retry. Returns
|
|
65
|
-
* neither is configured.
|
|
64
|
+
* Resolve the Kimi Code search credential. Highest precedence is the explicit
|
|
65
|
+
* search-key env override; otherwise an AuthStorage-backed resolver for a
|
|
66
|
+
* stored `kimi-code` credential (from `omp /login kimi-code`), so a stale token
|
|
67
|
+
* triggers the central force-refresh / sibling-rotate retry. Returns
|
|
68
|
+
* `undefined` when neither is configured.
|
|
69
|
+
*
|
|
70
|
+
* The endpoint (`https://api.kimi.com/coding/v1/search`) is the Kimi Code
|
|
71
|
+
* membership service, which has a different credential system from the Moonshot
|
|
72
|
+
* Open Platform (`https://api.moonshot.ai`). A stored `moonshot` credential
|
|
73
|
+
* (or `MOONSHOT_API_KEY`) is NOT accepted here — it 401s against Kimi Code
|
|
74
|
+
* (issue #5762).
|
|
66
75
|
*/
|
|
67
76
|
async function resolveKey(
|
|
68
77
|
authStorage: AuthStorage,
|
|
@@ -72,10 +81,8 @@ async function resolveKey(
|
|
|
72
81
|
const envKey = asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ?? asTrimmed($env.KIMI_SEARCH_API_KEY);
|
|
73
82
|
if (envKey) return envKey;
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (stored) return authStorage.resolver(provider, { sessionId });
|
|
78
|
-
}
|
|
84
|
+
const stored = await authStorage.getApiKey("kimi-code", sessionId, { signal });
|
|
85
|
+
if (stored) return authStorage.resolver("kimi-code", { sessionId });
|
|
79
86
|
return undefined;
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -127,7 +134,7 @@ export async function searchKimi(params: KimiSearchParams): Promise<SearchRespon
|
|
|
127
134
|
const keyOrResolver = await resolveKey(params.authStorage, params.sessionId, params.signal);
|
|
128
135
|
if (!keyOrResolver) {
|
|
129
136
|
throw new Error(
|
|
130
|
-
"Kimi search credentials not found.
|
|
137
|
+
"Kimi search credentials not found. Kimi web search uses the Kimi Code service (api.kimi.com); set MOONSHOT_SEARCH_API_KEY / KIMI_SEARCH_API_KEY to a Kimi Code Console key, or login with 'omp /login kimi-code'. A Moonshot Open Platform key (MOONSHOT_API_KEY) is not accepted here.",
|
|
131
138
|
);
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -176,7 +183,6 @@ export class KimiProvider extends SearchProvider {
|
|
|
176
183
|
return (
|
|
177
184
|
!!asTrimmed($env.MOONSHOT_SEARCH_API_KEY) ||
|
|
178
185
|
!!asTrimmed($env.KIMI_SEARCH_API_KEY) ||
|
|
179
|
-
authStorage.hasAuth("moonshot") ||
|
|
180
186
|
authStorage.hasAuth("kimi-code")
|
|
181
187
|
);
|
|
182
188
|
}
|
package/src/web/search/types.ts
CHANGED
|
@@ -39,7 +39,12 @@ export const SEARCH_PROVIDER_OPTIONS = [
|
|
|
39
39
|
{ value: "tavily", label: "Tavily", description: "Requires TAVILY_API_KEY" },
|
|
40
40
|
{ value: "firecrawl", label: "Firecrawl", description: "Requires FIRECRAWL_API_KEY" },
|
|
41
41
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
42
|
-
{
|
|
42
|
+
{
|
|
43
|
+
value: "kimi",
|
|
44
|
+
label: "Kimi",
|
|
45
|
+
description:
|
|
46
|
+
"Kimi Code search (requires a Kimi Code Console key via KIMI_SEARCH_API_KEY/MOONSHOT_SEARCH_API_KEY or /login kimi-code; not MOONSHOT_API_KEY)",
|
|
47
|
+
},
|
|
43
48
|
{ value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
|
|
44
49
|
{ value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
|
|
45
50
|
{ value: "searxng", label: "SearXNG", description: "Requires SEARXNG_ENDPOINT or searxng.endpoint" },
|