@oh-my-pi/pi-coding-agent 15.8.3 → 15.9.1
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 +113 -31
- package/dist/types/capability/skill.d.ts +7 -0
- package/dist/types/cli/update-cli.d.ts +15 -1
- package/dist/types/config/append-only-context-mode.d.ts +8 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +15 -0
- package/dist/types/config/settings-schema.d.ts +38 -24
- package/dist/types/debug/protocol-probe.d.ts +38 -0
- package/dist/types/debug/terminal-info.d.ts +34 -0
- package/dist/types/exa/mcp-client.d.ts +2 -1
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/mcp/json-rpc.d.ts +6 -1
- package/dist/types/mcp/tool-bridge.d.ts +4 -0
- package/dist/types/mcp/transports/stdio.d.ts +16 -7
- package/dist/types/mnemopi/state.d.ts +2 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/assistant-message.d.ts +3 -2
- package/dist/types/modes/components/extensions/extension-dashboard.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/components/plugin-settings.d.ts +40 -8
- package/dist/types/modes/components/session-selector.d.ts +8 -3
- package/dist/types/modes/components/settings-selector.d.ts +1 -1
- package/dist/types/modes/components/todo-reminder.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +36 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +1 -1
- package/dist/types/modes/theme/theme.d.ts +20 -1
- package/dist/types/modes/utils/keybinding-matchers.d.ts +4 -0
- package/dist/types/plan-mode/plan-handoff.d.ts +20 -0
- package/dist/types/session/agent-session.d.ts +5 -2
- package/dist/types/session/history-storage.d.ts +3 -4
- package/dist/types/session/indexed-session-storage.d.ts +59 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/redis-session-storage.d.ts +12 -85
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-storage.d.ts +5 -7
- package/dist/types/session/sql-session-storage.d.ts +16 -85
- package/dist/types/slash-commands/types.d.ts +17 -4
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/telemetry-export.d.ts +19 -0
- package/dist/types/tiny/compiled-runtime.d.ts +35 -0
- package/dist/types/tiny/text.d.ts +17 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/index.d.ts +4 -2
- package/dist/types/tools/path-utils.d.ts +1 -1
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/{todo-write.d.ts → todo.d.ts} +18 -18
- package/dist/types/utils/session-color.d.ts +7 -2
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +2 -2
- package/dist/types/web/search/providers/base.d.ts +14 -0
- package/dist/types/web/search/providers/exa.d.ts +9 -0
- package/dist/types/web/search/providers/perplexity.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +66 -1
- package/package.json +15 -9
- package/scripts/build-binary.ts +12 -0
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +1 -1
- package/src/cli/session-picker.ts +1 -0
- package/src/cli/update-cli.ts +54 -2
- package/src/commands/completions.ts +1 -1
- package/src/config/append-only-context-mode.ts +37 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +24 -57
- package/src/debug/index.ts +67 -1
- package/src/debug/protocol-probe.ts +267 -0
- package/src/debug/terminal-info.ts +127 -0
- package/src/exa/mcp-client.ts +11 -5
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +3 -3
- package/src/extensibility/custom-tools/types.ts +1 -1
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +3 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +16 -2
- package/src/mcp/json-rpc.ts +8 -0
- package/src/mcp/render.ts +3 -0
- package/src/mcp/tool-bridge.ts +10 -2
- package/src/mcp/transports/http.ts +33 -16
- package/src/mcp/transports/stdio.ts +37 -12
- package/src/mnemopi/state.ts +4 -4
- package/src/modes/acp/acp-agent.ts +168 -3
- package/src/modes/acp/acp-event-mapper.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +103 -31
- package/src/modes/components/assistant-message.ts +3 -2
- package/src/modes/components/extensions/extension-dashboard.ts +56 -10
- package/src/modes/components/history-search.ts +128 -14
- package/src/modes/components/hook-selector.ts +149 -14
- package/src/modes/components/plugin-settings.ts +270 -36
- package/src/modes/components/session-selector.ts +81 -19
- package/src/modes/components/settings-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +2 -1
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/components/tips.txt +6 -2
- package/src/modes/components/todo-reminder.ts +1 -1
- package/src/modes/components/tool-execution.ts +9 -4
- package/src/modes/components/transcript-container.ts +109 -0
- package/src/modes/controllers/command-controller.ts +4 -3
- package/src/modes/controllers/event-controller.ts +12 -7
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +10 -5
- package/src/modes/controllers/selector-controller.ts +30 -19
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +56 -9
- package/src/modes/print-mode.ts +5 -0
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/modes/theme/theme.ts +48 -8
- package/src/modes/utils/keybinding-matchers.ts +10 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/plan-mode/plan-handoff.ts +37 -0
- package/src/priority.json +4 -0
- package/src/prompts/goals/goal-continuation.md +1 -1
- package/src/prompts/steering/user-interjection.md +10 -0
- package/src/prompts/system/agent-creation-architect.md +1 -26
- package/src/prompts/system/eager-todo.md +3 -3
- package/src/prompts/system/orchestrate-notice.md +5 -5
- package/src/prompts/system/plan-mode-approved.md +14 -17
- package/src/prompts/system/plan-mode-reference.md +3 -6
- package/src/prompts/system/subagent-system-prompt.md +11 -0
- package/src/prompts/system/system-prompt.md +143 -145
- package/src/prompts/system/title-system.md +3 -2
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/browser.md +33 -30
- package/src/prompts/tools/render-mermaid.md +2 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +3 -1
- package/src/prompts/tools/{todo-write.md → todo.md} +5 -5
- package/src/sdk.ts +6 -21
- package/src/session/agent-session.ts +44 -21
- package/src/session/history-storage.ts +11 -18
- package/src/session/indexed-session-storage.ts +430 -0
- package/src/session/messages.ts +80 -0
- package/src/session/redis-session-storage.ts +66 -377
- package/src/session/session-manager.ts +109 -23
- package/src/session/session-storage.ts +148 -68
- package/src/session/sql-session-storage.ts +131 -382
- package/src/slash-commands/helpers/todo.ts +2 -2
- package/src/slash-commands/types.ts +27 -10
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +51 -1
- package/src/telemetry-export.ts +126 -0
- package/src/tiny/compiled-runtime.ts +179 -0
- package/src/tiny/text.ts +112 -1
- package/src/tiny/worker.ts +24 -2
- package/src/tools/ask.ts +133 -87
- package/src/tools/fetch.ts +17 -4
- package/src/tools/find.ts +2 -2
- package/src/tools/index.ts +6 -4
- package/src/tools/memory-recall.ts +1 -1
- package/src/tools/memory-reflect.ts +1 -1
- package/src/tools/path-utils.ts +16 -7
- package/src/tools/renderers.ts +2 -2
- package/src/tools/search.ts +6 -3
- package/src/tools/ssh.ts +26 -10
- package/src/tools/{todo-write.ts → todo.ts} +32 -35
- package/src/tools/write.ts +14 -2
- package/src/tui/status-line.ts +15 -4
- package/src/utils/session-color.ts +39 -14
- package/src/utils/title-generator.ts +9 -2
- package/src/web/search/index.ts +4 -2
- package/src/web/search/provider.ts +19 -35
- package/src/web/search/providers/base.ts +17 -0
- package/src/web/search/providers/exa.ts +111 -7
- package/src/web/search/providers/perplexity.ts +8 -4
- package/src/web/search/types.ts +74 -32
|
@@ -6,7 +6,7 @@ import chalk from "chalk";
|
|
|
6
6
|
import * as z from "zod/v4";
|
|
7
7
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
8
8
|
import type { Theme } from "../modes/theme/theme";
|
|
9
|
-
import
|
|
9
|
+
import todoDescription from "../prompts/tools/todo.md" with { type: "text" };
|
|
10
10
|
import type { ToolSession } from "../sdk";
|
|
11
11
|
import type { SessionEntry } from "../session/session-manager";
|
|
12
12
|
import { renderStatusLine, renderTreeList } from "../tui";
|
|
@@ -40,7 +40,7 @@ export interface TodoCompletionTransition {
|
|
|
40
40
|
content: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export interface
|
|
43
|
+
export interface TodoToolDetails {
|
|
44
44
|
phases: TodoPhase[];
|
|
45
45
|
storage: "session" | "memory";
|
|
46
46
|
completedTasks?: TodoCompletionTransition[];
|
|
@@ -68,14 +68,14 @@ const TodoOpEntry = z.object({
|
|
|
68
68
|
text: z.string().optional().describe("note text"),
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
const
|
|
71
|
+
const todoSchema = z
|
|
72
72
|
.object({
|
|
73
73
|
ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
|
|
74
74
|
})
|
|
75
75
|
.describe("apply ordered todo operations");
|
|
76
76
|
|
|
77
|
-
type
|
|
78
|
-
type TodoOpEntryValue =
|
|
77
|
+
type TodoParams = z.infer<typeof todoSchema>;
|
|
78
|
+
type TodoOpEntryValue = TodoParams["ops"][number];
|
|
79
79
|
|
|
80
80
|
// =============================================================================
|
|
81
81
|
// State helpers
|
|
@@ -159,7 +159,7 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
|
|
|
159
159
|
}
|
|
160
160
|
if (entry.type !== "message") continue;
|
|
161
161
|
const message = entry.message as { role?: string; toolName?: string; details?: unknown; isError?: boolean };
|
|
162
|
-
if (message.role !== "toolResult" || message.toolName !== "
|
|
162
|
+
if (message.role !== "toolResult" || message.toolName !== "todo" || message.isError) continue;
|
|
163
163
|
|
|
164
164
|
const details = message.details as { phases?: unknown } | undefined;
|
|
165
165
|
if (!details || !Array.isArray(details.phases)) continue;
|
|
@@ -178,7 +178,7 @@ export function getLatestTodoPhasesFromEntries(entries: SessionEntry[]): TodoPha
|
|
|
178
178
|
* the caller can render a `+N more` hint. When every task in `tasks` is
|
|
179
179
|
* closed (completed or abandoned), returns the trailing `maxVisible` tasks
|
|
180
180
|
* with `hiddenOpenCount = 0`, so the panel keeps useful context until the
|
|
181
|
-
* active-phase pointer advances on the next `
|
|
181
|
+
* active-phase pointer advances on the next `todo`.
|
|
182
182
|
*
|
|
183
183
|
* Task identity and order are preserved — this is a slice, never a sort.
|
|
184
184
|
*/
|
|
@@ -383,7 +383,7 @@ function applyEntry(phases: TodoPhase[], entry: TodoOpEntryValue, errors: string
|
|
|
383
383
|
}
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
function applyParams(phases: TodoPhase[], params:
|
|
386
|
+
function applyParams(phases: TodoPhase[], params: TodoParams): { phases: TodoPhase[]; errors: string[] } {
|
|
387
387
|
const errors: string[] = [];
|
|
388
388
|
let next = phases;
|
|
389
389
|
for (const entry of params.ops) {
|
|
@@ -393,10 +393,10 @@ function applyParams(phases: TodoPhase[], params: TodoWriteParams): { phases: To
|
|
|
393
393
|
return { phases: next, errors };
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
/** Apply an array of `
|
|
396
|
+
/** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
|
|
397
397
|
export function applyOpsToPhases(
|
|
398
398
|
currentPhases: TodoPhase[],
|
|
399
|
-
ops:
|
|
399
|
+
ops: TodoParams["ops"],
|
|
400
400
|
): { phases: TodoPhase[]; errors: string[] } {
|
|
401
401
|
return applyParams(clonePhases(currentPhases), { ops });
|
|
402
402
|
}
|
|
@@ -586,33 +586,33 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
|
|
586
586
|
// Tool Class
|
|
587
587
|
// =============================================================================
|
|
588
588
|
|
|
589
|
-
export class
|
|
590
|
-
readonly name = "
|
|
589
|
+
export class TodoTool implements AgentTool<typeof todoSchema, TodoToolDetails> {
|
|
590
|
+
readonly name = "todo";
|
|
591
591
|
readonly approval = "read" as const;
|
|
592
|
-
readonly label = "Todo
|
|
592
|
+
readonly label = "Todo";
|
|
593
593
|
readonly summary = "Write a structured todo list to track progress within a session";
|
|
594
594
|
readonly description: string;
|
|
595
|
-
readonly parameters =
|
|
595
|
+
readonly parameters = todoSchema;
|
|
596
596
|
readonly concurrency = "exclusive";
|
|
597
597
|
readonly strict = true;
|
|
598
598
|
readonly loadMode = "discoverable";
|
|
599
599
|
constructor(private readonly session: ToolSession) {
|
|
600
|
-
this.description = prompt.render(
|
|
600
|
+
this.description = prompt.render(todoDescription);
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
async execute(
|
|
604
604
|
_toolCallId: string,
|
|
605
|
-
params:
|
|
605
|
+
params: TodoParams,
|
|
606
606
|
_signal?: AbortSignal,
|
|
607
|
-
_onUpdate?: AgentToolUpdateCallback<
|
|
607
|
+
_onUpdate?: AgentToolUpdateCallback<TodoToolDetails>,
|
|
608
608
|
_context?: AgentToolContext,
|
|
609
|
-
): Promise<AgentToolResult<
|
|
609
|
+
): Promise<AgentToolResult<TodoToolDetails>> {
|
|
610
610
|
const previousPhases = clonePhases(this.session.getTodoPhases?.() ?? []);
|
|
611
611
|
const { phases: updated, errors } = applyParams(clonePhases(previousPhases), params);
|
|
612
612
|
const completedTasks = getCompletionTransitions(previousPhases, updated);
|
|
613
613
|
this.session.setTodoPhases?.(updated);
|
|
614
614
|
const storage = this.session.getSessionFile() ? "session" : "memory";
|
|
615
|
-
const details:
|
|
615
|
+
const details: TodoToolDetails = { phases: updated, storage };
|
|
616
616
|
if (completedTasks.length > 0) details.completedTasks = completedTasks;
|
|
617
617
|
|
|
618
618
|
return {
|
|
@@ -627,7 +627,7 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
|
|
|
627
627
|
// TUI Renderer
|
|
628
628
|
// =============================================================================
|
|
629
629
|
|
|
630
|
-
type
|
|
630
|
+
type TodoRenderArgs = {
|
|
631
631
|
ops?: Array<{
|
|
632
632
|
op?: string;
|
|
633
633
|
task?: string;
|
|
@@ -701,9 +701,9 @@ function noteMarker(count: number, uiTheme: Theme): string {
|
|
|
701
701
|
return uiTheme.fg("dim", chalk.italic(` \u207a${toSuperscript(count)}`));
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
-
export const
|
|
705
|
-
export const
|
|
706
|
-
export const
|
|
704
|
+
export const TODO_STRIKE_HOLD_FRAMES = 2;
|
|
705
|
+
export const TODO_STRIKE_REVEAL_FRAMES = 12;
|
|
706
|
+
export const TODO_STRIKE_TOTAL_FRAMES = TODO_STRIKE_HOLD_FRAMES + TODO_STRIKE_REVEAL_FRAMES;
|
|
707
707
|
const EMPTY_COMPLETION_KEYS = new Set<string>();
|
|
708
708
|
const STRIKE_START = "\x1b[9m";
|
|
709
709
|
const STRIKE_END = "\x1b[29m";
|
|
@@ -721,11 +721,11 @@ function partialStrikethrough(text: string, visibleChars: number): string {
|
|
|
721
721
|
|
|
722
722
|
function strikeRevealCount(text: string, frame: number | undefined): number | undefined {
|
|
723
723
|
if (frame === undefined) return undefined;
|
|
724
|
-
if (frame <=
|
|
724
|
+
if (frame <= TODO_STRIKE_HOLD_FRAMES) return 0;
|
|
725
725
|
const chars = [...text];
|
|
726
726
|
if (chars.length === 0) return undefined;
|
|
727
|
-
const revealFrame = Math.min(frame -
|
|
728
|
-
return Math.ceil((chars.length * revealFrame) /
|
|
727
|
+
const revealFrame = Math.min(frame - TODO_STRIKE_HOLD_FRAMES, TODO_STRIKE_REVEAL_FRAMES);
|
|
728
|
+
return Math.ceil((chars.length * revealFrame) / TODO_STRIKE_REVEAL_FRAMES);
|
|
729
729
|
}
|
|
730
730
|
|
|
731
731
|
function formatTodoLine(
|
|
@@ -775,8 +775,8 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
775
775
|
return lines;
|
|
776
776
|
}
|
|
777
777
|
|
|
778
|
-
export const
|
|
779
|
-
renderCall(args:
|
|
778
|
+
export const todoToolRenderer = {
|
|
779
|
+
renderCall(args: TodoRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
780
780
|
const ops = args?.ops?.map(entry => {
|
|
781
781
|
const parts = [entry.op ?? "update"];
|
|
782
782
|
if (entry.task) parts.push(entry.task);
|
|
@@ -784,15 +784,15 @@ export const todoWriteToolRenderer = {
|
|
|
784
784
|
if (entry.items?.length) parts.push(`${entry.items.length} item${entry.items.length === 1 ? "" : "s"}`);
|
|
785
785
|
return parts.join(" ");
|
|
786
786
|
}) ?? ["update"];
|
|
787
|
-
const text = renderStatusLine({ icon: "pending", title: "Todo
|
|
787
|
+
const text = renderStatusLine({ icon: "pending", title: "Todo", meta: ops }, uiTheme);
|
|
788
788
|
return new Text(text, 0, 0);
|
|
789
789
|
},
|
|
790
790
|
|
|
791
791
|
renderResult(
|
|
792
|
-
result: { content: Array<{ type: string; text?: string }>; details?:
|
|
792
|
+
result: { content: Array<{ type: string; text?: string }>; details?: TodoToolDetails },
|
|
793
793
|
options: RenderResultOptions,
|
|
794
794
|
uiTheme: Theme,
|
|
795
|
-
_args?:
|
|
795
|
+
_args?: TodoRenderArgs,
|
|
796
796
|
): Component {
|
|
797
797
|
const phases = (result.details?.phases ?? []).filter(phase => phase.tasks.length > 0);
|
|
798
798
|
const completedTasks = result.details?.completedTasks ?? [];
|
|
@@ -806,10 +806,7 @@ export const todoWriteToolRenderer = {
|
|
|
806
806
|
keys.add(task.content);
|
|
807
807
|
}
|
|
808
808
|
const allTasks = phases.flatMap(phase => phase.tasks);
|
|
809
|
-
const header = renderStatusLine(
|
|
810
|
-
{ icon: "success", title: "Todo Write", meta: [`${allTasks.length} tasks`] },
|
|
811
|
-
uiTheme,
|
|
812
|
-
);
|
|
809
|
+
const header = renderStatusLine({ icon: "success", title: "Todo", meta: [`${allTasks.length} tasks`] }, uiTheme);
|
|
813
810
|
if (allTasks.length === 0) {
|
|
814
811
|
const fallback = result.content?.find(content => content.type === "text")?.text ?? "No todos";
|
|
815
812
|
return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
|
package/src/tools/write.ts
CHANGED
|
@@ -135,6 +135,15 @@ function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, co
|
|
|
135
135
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function shouldRouteWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string): boolean {
|
|
139
|
+
if (isInternalUrlPath(requestedPath)) return false;
|
|
140
|
+
|
|
141
|
+
const state = session.getPlanModeState?.();
|
|
142
|
+
if (!state?.enabled || !isInternalUrlPath(state.planFilePath)) return true;
|
|
143
|
+
|
|
144
|
+
return absolutePath !== resolvePlanPath(session, state.planFilePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
/**
|
|
139
148
|
* Append a trailing note line to the first text block of a tool result.
|
|
140
149
|
* Mutates `result` in place (the result object is owned by this call).
|
|
@@ -845,8 +854,11 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
845
854
|
await assertEditableFile(absolutePath, path);
|
|
846
855
|
}
|
|
847
856
|
|
|
848
|
-
// Try ACP bridge first
|
|
849
|
-
|
|
857
|
+
// Try ACP bridge first for editor-visible filesystem paths. Internal
|
|
858
|
+
// artifacts such as local:// plans are owned by OMP, not the editor.
|
|
859
|
+
const bridgePromise = shouldRouteWriteThroughBridge(this.session, path, absolutePath)
|
|
860
|
+
? this.#routeWriteThroughBridge(absolutePath, cleanContent)
|
|
861
|
+
: undefined;
|
|
850
862
|
if (bridgePromise !== undefined) {
|
|
851
863
|
try {
|
|
852
864
|
await bridgePromise;
|
package/src/tui/status-line.ts
CHANGED
|
@@ -15,22 +15,33 @@ export interface StatusLineOptions {
|
|
|
15
15
|
meta?: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Flatten CR/LF runs in caller-supplied header fragments so a single newline
|
|
20
|
+
* embedded in `description` or `meta` cannot expand the status line into
|
|
21
|
+
* multiple rows — which would otherwise break the bordered output block the
|
|
22
|
+
* header sits on. Tab characters are left alone; tool renderers that need
|
|
23
|
+
* tab-safe text run `replaceTabs()` themselves.
|
|
24
|
+
*/
|
|
25
|
+
function flattenForHeader(text: string): string {
|
|
26
|
+
return text.replace(/\r\n?|\n/g, " ");
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
|
|
19
30
|
const icon = options.icon ? formatStatusIcon(options.icon, theme, options.spinnerFrame) : "";
|
|
20
31
|
const titleColor = options.titleColor ?? "accent";
|
|
21
|
-
const title = theme.fg(titleColor, options.title);
|
|
32
|
+
const title = theme.fg(titleColor, flattenForHeader(options.title));
|
|
22
33
|
let line = icon ? `${icon} ${title}` : title;
|
|
23
34
|
|
|
24
35
|
if (options.description) {
|
|
25
|
-
line += `: ${theme.fg("muted", options.description)}`;
|
|
36
|
+
line += `: ${theme.fg("muted", flattenForHeader(options.description))}`;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
if (options.badge) {
|
|
29
40
|
const { label, color } = options.badge;
|
|
30
|
-
line += ` ${theme.fg(color, `${theme.format.bracketLeft}${label}${theme.format.bracketRight}`)}`;
|
|
41
|
+
line += ` ${theme.fg(color, `${theme.format.bracketLeft}${flattenForHeader(label)}${theme.format.bracketRight}`)}`;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
|
-
const meta = options.meta?.filter(value => value.trim().length > 0) ?? [];
|
|
44
|
+
const meta = options.meta?.map(flattenForHeader).filter(value => value.trim().length > 0) ?? [];
|
|
34
45
|
if (meta.length > 0) {
|
|
35
46
|
line += ` ${theme.fg("dim", meta.join(theme.sep.dot))}`;
|
|
36
47
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hslToHex, relativeLuminance } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Derive a stable hue (0-359) from a string using djb2 hash.
|
|
3
5
|
*/
|
|
@@ -10,27 +12,50 @@ function nameToHue(name: string): number {
|
|
|
10
12
|
return hash % 360;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
const ACCENT_SATURATION = 0.9;
|
|
16
|
+
const ACCENT_DARK_LIGHTNESS = 0.72;
|
|
17
|
+
/** Minimum contrast ratio (WCAG AA large text) between a light-theme accent and its surface. */
|
|
18
|
+
const ACCENT_MIN_CONTRAST = 3;
|
|
19
|
+
|
|
13
20
|
/**
|
|
14
|
-
*
|
|
21
|
+
* Largest relative luminance an accent may have while still meeting
|
|
22
|
+
* {@link ACCENT_MIN_CONTRAST} against a surface of the given luminance.
|
|
15
23
|
*/
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
const f = (n: number) => {
|
|
19
|
-
const k = (n + h / 30) % 12;
|
|
20
|
-
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
21
|
-
return Math.round(255 * color)
|
|
22
|
-
.toString(16)
|
|
23
|
-
.padStart(2, "0");
|
|
24
|
-
};
|
|
25
|
-
return `#${f(0)}${f(8)}${f(4)}`;
|
|
24
|
+
function accentLuminanceCap(surfaceLuminance: number): number {
|
|
25
|
+
return Math.max(0, (surfaceLuminance + 0.05) / ACCENT_MIN_CONTRAST - 0.05);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Derive a stable CSS hex accent color from a session name.
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
|
+
* On dark themes (`surfaceLuminance` undefined) the accent is vivid (high
|
|
32
|
+
* saturation, high lightness). On light themes the lightness is reduced until the
|
|
33
|
+
* accent's perceived luminance clears {@link ACCENT_MIN_CONTRAST} against the
|
|
34
|
+
* actual surface it renders on — so it stays legible on near-white *and* mid-light
|
|
35
|
+
* backgrounds — while keeping the same per-session hue.
|
|
31
36
|
*/
|
|
32
|
-
export function getSessionAccentHex(name: string): string {
|
|
33
|
-
|
|
37
|
+
export function getSessionAccentHex(name: string, surfaceLuminance?: number): string {
|
|
38
|
+
const hue = nameToHue(name);
|
|
39
|
+
if (surfaceLuminance === undefined) {
|
|
40
|
+
return hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cap = accentLuminanceCap(surfaceLuminance);
|
|
44
|
+
const top = hslToHex(hue, ACCENT_SATURATION, ACCENT_DARK_LIGHTNESS);
|
|
45
|
+
if ((relativeLuminance(top) ?? 0) <= cap) return top;
|
|
46
|
+
|
|
47
|
+
// Bisect lightness: `lo` always yields luminance <= cap, `hi` always above it.
|
|
48
|
+
let lo = 0;
|
|
49
|
+
let hi = ACCENT_DARK_LIGHTNESS;
|
|
50
|
+
for (let i = 0; i < 20; i++) {
|
|
51
|
+
const mid = (lo + hi) / 2;
|
|
52
|
+
if ((relativeLuminance(hslToHex(hue, ACCENT_SATURATION, mid)) ?? 0) > cap) {
|
|
53
|
+
hi = mid;
|
|
54
|
+
} else {
|
|
55
|
+
lo = mid;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return hslToHex(hue, ACCENT_SATURATION, lo);
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
/**
|
|
@@ -10,7 +10,7 @@ import { resolveRoleSelection } from "../config/model-resolver";
|
|
|
10
10
|
import type { Settings } from "../config/settings";
|
|
11
11
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
12
12
|
import { ONLINE_TINY_TITLE_MODEL_KEY } from "../tiny/models";
|
|
13
|
-
import { formatTitleUserMessage, normalizeGeneratedTitle } from "../tiny/text";
|
|
13
|
+
import { formatTitleUserMessage, isLowSignalTitleInput, normalizeGeneratedTitle } from "../tiny/text";
|
|
14
14
|
import { tinyTitleClient } from "../tiny/title-client";
|
|
15
15
|
|
|
16
16
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
@@ -31,7 +31,8 @@ const setTitleTool: Tool = {
|
|
|
31
31
|
properties: {
|
|
32
32
|
title: {
|
|
33
33
|
type: "string",
|
|
34
|
-
description:
|
|
34
|
+
description:
|
|
35
|
+
'A concise 3-6 word title for the session, or exactly "none" when the message carries no concrete task yet (greeting, small talk, vague).',
|
|
35
36
|
},
|
|
36
37
|
},
|
|
37
38
|
required: ["title"],
|
|
@@ -144,6 +145,12 @@ export async function generateSessionTitle(
|
|
|
144
145
|
currentModel?: Model<Api>,
|
|
145
146
|
metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
|
|
146
147
|
): Promise<string | null> {
|
|
148
|
+
// Defer titling for greetings / acknowledgements / empty input. The default
|
|
149
|
+
// tiny title model can't reliably decline trivial input, so this happens
|
|
150
|
+
// deterministically before any model is invoked; the caller retries on the
|
|
151
|
+
// next user message while the session stays unnamed.
|
|
152
|
+
if (isLowSignalTitleInput(firstMessage)) return null;
|
|
153
|
+
|
|
147
154
|
const tinyModel = settings.get("providers.tinyModel");
|
|
148
155
|
if (tinyModel === ONLINE_TINY_TITLE_MODEL_KEY) {
|
|
149
156
|
return generateTitleOnline(firstMessage, registry, settings, sessionId, currentModel, metadataResolver);
|
package/src/web/search/index.ts
CHANGED
|
@@ -131,7 +131,9 @@ async function executeSearch(
|
|
|
131
131
|
const providers =
|
|
132
132
|
params.provider && params.provider !== "auto"
|
|
133
133
|
? await getSearchProvider(params.provider).then(async provider =>
|
|
134
|
-
(await provider.
|
|
134
|
+
(await provider.isExplicitlyAvailable(authStorage))
|
|
135
|
+
? [provider]
|
|
136
|
+
: resolveProviderChain(authStorage, "auto"),
|
|
135
137
|
)
|
|
136
138
|
: await resolveProviderChain(authStorage);
|
|
137
139
|
if (providers.length === 0) {
|
|
@@ -220,7 +222,7 @@ export async function runSearchQuery(
|
|
|
220
222
|
/**
|
|
221
223
|
* Web search tool implementation.
|
|
222
224
|
*
|
|
223
|
-
* Supports
|
|
225
|
+
* Supports the configured web-search provider chain with automatic fallback.
|
|
224
226
|
*/
|
|
225
227
|
export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRenderDetails> {
|
|
226
228
|
readonly name = "web_search";
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
// fetch/parse/format helpers) and only one — at most — is needed per session,
|
|
6
6
|
// so eager construction was wasted work at startup.
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// Provider modules are loaded lazily; display metadata lives in types.ts so UI
|
|
9
|
+
// listings can share it without importing provider implementations.
|
|
10
10
|
|
|
11
11
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import type { SearchProvider } from "./providers/base";
|
|
13
|
-
import type
|
|
13
|
+
import { SEARCH_PROVIDER_LABELS, SEARCH_PROVIDER_ORDER, type SearchProviderId } from "./types";
|
|
14
14
|
|
|
15
15
|
export type { SearchParams } from "./providers/base";
|
|
16
16
|
export { SearchProvider } from "./providers/base";
|
|
17
|
+
export { SEARCH_PROVIDER_ORDER } from "./types";
|
|
17
18
|
|
|
18
19
|
interface ProviderMeta {
|
|
19
20
|
id: SearchProviderId;
|
|
@@ -25,72 +26,72 @@ interface ProviderMeta {
|
|
|
25
26
|
const PROVIDER_META: Record<SearchProviderId, ProviderMeta> = {
|
|
26
27
|
exa: {
|
|
27
28
|
id: "exa",
|
|
28
|
-
label:
|
|
29
|
+
label: SEARCH_PROVIDER_LABELS.exa,
|
|
29
30
|
load: async () => new (await import("./providers/exa")).ExaProvider(),
|
|
30
31
|
},
|
|
31
32
|
brave: {
|
|
32
33
|
id: "brave",
|
|
33
|
-
label:
|
|
34
|
+
label: SEARCH_PROVIDER_LABELS.brave,
|
|
34
35
|
load: async () => new (await import("./providers/brave")).BraveProvider(),
|
|
35
36
|
},
|
|
36
37
|
jina: {
|
|
37
38
|
id: "jina",
|
|
38
|
-
label:
|
|
39
|
+
label: SEARCH_PROVIDER_LABELS.jina,
|
|
39
40
|
load: async () => new (await import("./providers/jina")).JinaProvider(),
|
|
40
41
|
},
|
|
41
42
|
perplexity: {
|
|
42
43
|
id: "perplexity",
|
|
43
|
-
label:
|
|
44
|
+
label: SEARCH_PROVIDER_LABELS.perplexity,
|
|
44
45
|
load: async () => new (await import("./providers/perplexity")).PerplexityProvider(),
|
|
45
46
|
},
|
|
46
47
|
kimi: {
|
|
47
48
|
id: "kimi",
|
|
48
|
-
label:
|
|
49
|
+
label: SEARCH_PROVIDER_LABELS.kimi,
|
|
49
50
|
load: async () => new (await import("./providers/kimi")).KimiProvider(),
|
|
50
51
|
},
|
|
51
52
|
zai: {
|
|
52
53
|
id: "zai",
|
|
53
|
-
label:
|
|
54
|
+
label: SEARCH_PROVIDER_LABELS.zai,
|
|
54
55
|
load: async () => new (await import("./providers/zai")).ZaiProvider(),
|
|
55
56
|
},
|
|
56
57
|
anthropic: {
|
|
57
58
|
id: "anthropic",
|
|
58
|
-
label:
|
|
59
|
+
label: SEARCH_PROVIDER_LABELS.anthropic,
|
|
59
60
|
load: async () => new (await import("./providers/anthropic")).AnthropicProvider(),
|
|
60
61
|
},
|
|
61
62
|
gemini: {
|
|
62
63
|
id: "gemini",
|
|
63
|
-
label:
|
|
64
|
+
label: SEARCH_PROVIDER_LABELS.gemini,
|
|
64
65
|
load: async () => new (await import("./providers/gemini")).GeminiProvider(),
|
|
65
66
|
},
|
|
66
67
|
codex: {
|
|
67
68
|
id: "codex",
|
|
68
|
-
label:
|
|
69
|
+
label: SEARCH_PROVIDER_LABELS.codex,
|
|
69
70
|
load: async () => new (await import("./providers/codex")).CodexProvider(),
|
|
70
71
|
},
|
|
71
72
|
tavily: {
|
|
72
73
|
id: "tavily",
|
|
73
|
-
label:
|
|
74
|
+
label: SEARCH_PROVIDER_LABELS.tavily,
|
|
74
75
|
load: async () => new (await import("./providers/tavily")).TavilyProvider(),
|
|
75
76
|
},
|
|
76
77
|
parallel: {
|
|
77
78
|
id: "parallel",
|
|
78
|
-
label:
|
|
79
|
+
label: SEARCH_PROVIDER_LABELS.parallel,
|
|
79
80
|
load: async () => new (await import("./providers/parallel")).ParallelProvider(),
|
|
80
81
|
},
|
|
81
82
|
kagi: {
|
|
82
83
|
id: "kagi",
|
|
83
|
-
label:
|
|
84
|
+
label: SEARCH_PROVIDER_LABELS.kagi,
|
|
84
85
|
load: async () => new (await import("./providers/kagi")).KagiProvider(),
|
|
85
86
|
},
|
|
86
87
|
synthetic: {
|
|
87
88
|
id: "synthetic",
|
|
88
|
-
label:
|
|
89
|
+
label: SEARCH_PROVIDER_LABELS.synthetic,
|
|
89
90
|
load: async () => new (await import("./providers/synthetic")).SyntheticProvider(),
|
|
90
91
|
},
|
|
91
92
|
searxng: {
|
|
92
93
|
id: "searxng",
|
|
93
|
-
label:
|
|
94
|
+
label: SEARCH_PROVIDER_LABELS.searxng,
|
|
94
95
|
load: async () => new (await import("./providers/searxng")).SearXNGProvider(),
|
|
95
96
|
},
|
|
96
97
|
};
|
|
@@ -118,23 +119,6 @@ export async function getSearchProvider(id: SearchProviderId): Promise<SearchPro
|
|
|
118
119
|
return provider;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
export const SEARCH_PROVIDER_ORDER: SearchProviderId[] = [
|
|
122
|
-
"tavily",
|
|
123
|
-
"perplexity",
|
|
124
|
-
"brave",
|
|
125
|
-
"jina",
|
|
126
|
-
"kimi",
|
|
127
|
-
"anthropic",
|
|
128
|
-
"gemini",
|
|
129
|
-
"codex",
|
|
130
|
-
"zai",
|
|
131
|
-
"exa",
|
|
132
|
-
"parallel",
|
|
133
|
-
"kagi",
|
|
134
|
-
"synthetic",
|
|
135
|
-
"searxng",
|
|
136
|
-
];
|
|
137
|
-
|
|
138
122
|
/** Preferred provider set via settings (default: auto) */
|
|
139
123
|
let preferredProvId: SearchProviderId | "auto" = "auto";
|
|
140
124
|
|
|
@@ -156,7 +140,7 @@ export async function resolveProviderChain(
|
|
|
156
140
|
|
|
157
141
|
if (preferredProvider !== "auto") {
|
|
158
142
|
const provider = await getSearchProvider(preferredProvider);
|
|
159
|
-
if (await provider.
|
|
143
|
+
if (await provider.isExplicitlyAvailable(authStorage)) {
|
|
160
144
|
providers.push(provider);
|
|
161
145
|
}
|
|
162
146
|
}
|
|
@@ -61,9 +61,26 @@ export abstract class SearchProvider {
|
|
|
61
61
|
* Indicates whether this provider has the credentials/config it needs to
|
|
62
62
|
* service a request right now. Implementations consult the passed
|
|
63
63
|
* {@link AuthStorage} — never a sibling store.
|
|
64
|
+
*
|
|
65
|
+
* Drives auto-chain admission: providers that return `false` are skipped
|
|
66
|
+
* when {@link resolveProviderChain} walks the order. Explicit selection
|
|
67
|
+
* uses {@link isExplicitlyAvailable} instead.
|
|
64
68
|
*/
|
|
65
69
|
abstract isAvailable(authStorage: AuthStorage): Promise<boolean> | boolean;
|
|
66
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Returns `true` when this provider should run when the user explicitly
|
|
73
|
+
* selects it, even if {@link isAvailable} would reject it for the auto
|
|
74
|
+
* chain. Providers that ship an unauthenticated fallback (e.g. Exa's
|
|
75
|
+
* public MCP) override this so explicit selection still routes through
|
|
76
|
+
* the fallback rather than silently falling back to another provider.
|
|
77
|
+
*
|
|
78
|
+
* Defaults to mirroring {@link isAvailable}.
|
|
79
|
+
*/
|
|
80
|
+
isExplicitlyAvailable(authStorage: AuthStorage): Promise<boolean> | boolean {
|
|
81
|
+
return this.isAvailable(authStorage);
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Execute a search. Credentials MUST be resolved through `params.authStorage`.
|
|
69
86
|
*/
|