pi-spark 0.14.7 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +26 -20
  2. package/assets/screenshot-credits.png +0 -0
  3. package/index.ts +2 -4
  4. package/package.json +3 -4
  5. package/src/config/index.ts +11 -45
  6. package/src/config/schema.ts +2 -4
  7. package/src/features/credits/providers/moonshot.ts +1 -1
  8. package/src/features/credits/status.ts +1 -1
  9. package/src/features/credits/types.ts +3 -3
  10. package/src/features/footer/index.ts +9 -2
  11. package/src/features/fullscreen/filler.ts +1 -1
  12. package/src/features/presets/manager.ts +1 -1
  13. package/src/features/presets/selector.ts +2 -2
  14. package/src/features/recap/manager.ts +6 -6
  15. package/src/features/title/config.ts +7 -0
  16. package/src/features/title/index.ts +26 -0
  17. package/src/features/title/manager.ts +117 -0
  18. package/src/utils/format.ts +2 -20
  19. package/src/{features/recap → utils}/model.ts +25 -10
  20. package/src/utils/usage.ts +1 -1
  21. package/assets/screenshot-tools.png +0 -0
  22. package/src/features/pi/actions/models.ts +0 -165
  23. package/src/features/pi/actions/name.ts +0 -58
  24. package/src/features/pi/actions/whoami.ts +0 -61
  25. package/src/features/pi/config.ts +0 -3
  26. package/src/features/pi/index.ts +0 -23
  27. package/src/features/pi/model.ts +0 -56
  28. package/src/features/pi/registry.ts +0 -28
  29. package/src/features/web/actions/fetch.ts +0 -47
  30. package/src/features/web/actions/search.ts +0 -50
  31. package/src/features/web/client.ts +0 -77
  32. package/src/features/web/config.ts +0 -3
  33. package/src/features/web/index.ts +0 -28
  34. package/src/features/web/registry.ts +0 -22
  35. package/src/features/web/render.ts +0 -34
  36. package/src/utils/tool.ts +0 -221
@@ -1,28 +0,0 @@
1
- import { ExaClient } from "./client";
2
- import { fetchAction } from "./actions/fetch";
3
- import { searchAction } from "./actions/search";
4
- import { registerWebTool } from "./registry";
5
- import { loadConfig } from "../../config";
6
-
7
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
-
9
- /** Web actions exposed by the web tool, ordered to match the description (search before fetch). */
10
- const ACTIONS = [
11
- searchAction,
12
- fetchAction,
13
- ];
14
-
15
- export function registerWeb(pi: ExtensionAPI): void {
16
- const exa = new ExaClient();
17
-
18
- pi.on("session_start", (_event, ctx) => {
19
- const config = loadConfig(ctx).web;
20
- if (!config) return;
21
-
22
- registerWebTool(pi, exa, ACTIONS);
23
- });
24
-
25
- pi.on("session_shutdown", () => {
26
- exa.close();
27
- });
28
- }
@@ -1,22 +0,0 @@
1
- import { defineActionFor, registerComposedTool } from "../../utils/tool";
2
-
3
- import type { ExaClient } from "./client";
4
- import type { Action } from "../../utils/tool";
5
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
-
7
- interface WebActionContext {
8
- exa: ExaClient;
9
- }
10
-
11
- export const defineAction = defineActionFor<WebActionContext>();
12
-
13
- export function registerWebTool(pi: ExtensionAPI, exa: ExaClient, actions: Action<WebActionContext, any, any>[]): void {
14
- registerComposedTool<WebActionContext>(pi, {
15
- name: "web",
16
- label: "web",
17
- descriptionIntro: "Access the live web via Exa:",
18
- promptSnippet: "Search the web and fetch page content via Exa",
19
- actions,
20
- createContext: () => ({ exa }),
21
- });
22
- }
@@ -1,34 +0,0 @@
1
- import { keyHint } from "@earendil-works/pi-coding-agent";
2
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
-
4
- import { joinTextContent } from "../../utils/format";
5
-
6
- import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
7
-
8
- const COLLAPSED_MAX_LINES = 10;
9
-
10
- /**
11
- * Render a web result's text content with collapse-to-expand behavior. Shared by the `search` and
12
- * `fetch` actions, which both return plain text content. Errors are handled by the registry's
13
- * fallback renderer, so this only covers the success path.
14
- */
15
- export function renderWebResult(result: AgentToolResult<unknown>, expanded: boolean, theme: Theme): Container {
16
- const container = new Container();
17
- container.addChild(new Spacer(1));
18
-
19
- const text = joinTextContent(result.content);
20
- const lines = text.length > 0 ? text.split("\n") : [];
21
-
22
- if (lines.length === 0) {
23
- container.addChild(new Text(theme.fg("muted", "No content returned."), 0, 0));
24
- return container;
25
- }
26
-
27
- const maxLines = expanded ? lines.length : Math.min(lines.length, COLLAPSED_MAX_LINES);
28
- container.addChild(new Text(theme.fg("muted", lines.slice(0, maxLines).join("\n")), 0, 0));
29
-
30
- const hidden = lines.length - maxLines;
31
- if (hidden > 0) container.addChild(new Text(theme.fg("dim", `... (${hidden} more lines, `) + keyHint("app.tools.expand", "to expand") + theme.fg("dim", ")"), 0, 0));
32
-
33
- return container;
34
- }
package/src/utils/tool.ts DELETED
@@ -1,221 +0,0 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
- import { Type } from "typebox";
4
-
5
- import { formatDuration, joinTextContent } from "./format";
6
-
7
- import type { AgentToolResult, AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
8
- import type { Component } from "@earendil-works/pi-tui";
9
- import type { Static, TObject, TProperties, TSchema } from "typebox";
10
-
11
- interface ActionDetails {
12
- action: string;
13
- }
14
-
15
- /**
16
- * One action of a composed tool. The registry merges all actions' `fields` into one flat schema
17
- * (provider-safe, unlike a discriminated union) and dispatches by `action`; `C` is the per-call
18
- * context from `createContext`.
19
- */
20
- export interface Action<C, F extends TProperties = TProperties, D extends ActionDetails = ActionDetails> {
21
- name: D["action"];
22
- summary: string;
23
- fields: F;
24
- /** Fields required at runtime (the flat schema makes all fields optional). */
25
- required?: (keyof F & string)[];
26
- promptGuidelines?: string[];
27
- /** Show bash-style elapsed/took timing for this action. */
28
- showTiming?: boolean;
29
- /** Styled segments shown after the `<tool> <action>` prefix. */
30
- renderParams?: (args: Static<TObject<F>>, theme: Theme) => string[];
31
- /** Only handles successful output; error output is handled centrally. */
32
- renderResult?: NonNullable<ToolDefinition<TObject<F>, D>["renderResult"]>;
33
- execute(args: Static<TObject<F>>, context: C, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<D> | undefined): Promise<AgentToolResult<D>>;
34
- }
35
-
36
- /** Identity helper bound to context `C`, so actions infer their field/details types while sharing one context shape. */
37
- export function defineActionFor<C>() {
38
- return <F extends TProperties, D extends ActionDetails>(action: Action<C, F, D>): Action<C, F, D> => action;
39
- }
40
-
41
- interface ComposedToolConfig<C> {
42
- name: string;
43
- label: string;
44
- descriptionIntro: string;
45
- descriptionOutro?: string;
46
- promptSnippet: string;
47
- generalGuidelines?: string[];
48
- actions: Action<C, any, any>[];
49
- createContext(ctx: ExtensionContext): C;
50
- }
51
-
52
- export function registerComposedTool<C>(pi: ExtensionAPI, config: ComposedToolConfig<C>): void {
53
- const byName = new Map<string, Action<C, any, any>>();
54
- const mergedFields: TProperties = {};
55
-
56
- for (const action of config.actions) {
57
- if (byName.has(action.name)) throw new Error(`Duplicate "${config.name}" action "${action.name}"`);
58
- byName.set(action.name, action);
59
-
60
- for (const [key, schema] of Object.entries(action.fields)) {
61
- if (key === "action") throw new Error(`"${config.name}" action "${action.name}" must not define a field named "action"`);
62
- if (key in mergedFields) throw new Error(`"${config.name}" action "${action.name}" redefines field "${key}"; field names must be unique across actions`);
63
- mergedFields[key] = Type.Optional(schema as TSchema);
64
- }
65
- }
66
-
67
- const summaries = config.actions.map((action) => `"${action.name}" ${action.summary}`).join("; ");
68
- const description = config.descriptionOutro
69
- ? `${config.descriptionIntro} ${summaries}. ${config.descriptionOutro}`
70
- : `${config.descriptionIntro} ${summaries}.`;
71
-
72
- const parameters = Type.Object({
73
- action: StringEnum(config.actions.map((action) => action.name), { description: `The ${config.name} action to run.` }),
74
- ...mergedFields,
75
- });
76
-
77
- const activeTiming = new ActiveTiming();
78
- const noTiming = new NoTiming();
79
- const timingFor = (action: string | undefined): Timing => (byName.get(action ?? "")?.showTiming ? activeTiming : noTiming);
80
-
81
- const renderActionResult: NonNullable<ToolDefinition["renderResult"]> = (result, options, theme, context) => {
82
- const details = result.details as ActionDetails | undefined;
83
-
84
- if (context.isError || !details) {
85
- const output = joinTextContent(result.content);
86
- return new Text(context.isError && output ? theme.fg("error", "\n" + output) : "", 0, 0);
87
- }
88
-
89
- const action = byName.get(details.action);
90
- if (action?.renderResult) return action.renderResult(result as AgentToolResult<ActionDetails>, options, theme, context as never);
91
-
92
- return new Text(joinTextContent(result.content), 0, 0);
93
- };
94
-
95
- pi.registerTool({
96
- name: config.name,
97
- label: config.label,
98
- description,
99
- promptSnippet: config.promptSnippet,
100
- promptGuidelines: [...(config.generalGuidelines ?? []), ...config.actions.flatMap((action) => action.promptGuidelines ?? [])],
101
- parameters,
102
- renderCall(args, theme, context) {
103
- const segments = [theme.bold(theme.fg("toolTitle", config.name)), theme.fg("accent", args.action)];
104
- const params = byName.get(args.action)?.renderParams?.(args, theme);
105
- if (params) segments.push(...params);
106
-
107
- return timingFor(context.args.action).renderCall(new Text(segments.join(" "), 0, 0), theme, context);
108
- },
109
- renderResult(result, options, theme, context) {
110
- const inner = renderActionResult(result, options, theme, context);
111
-
112
- return timingFor(context.args.action).renderResult(inner, options, theme, context);
113
- },
114
- async execute(_toolCallId, params, signal, onUpdate, ctx) {
115
- const action = byName.get(params.action);
116
- if (!action) throw new Error(`Unknown ${config.name} action "${params.action}"`);
117
-
118
- for (const field of action.required ?? []) {
119
- if ((params as Record<string, unknown>)[field] === undefined) {
120
- throw new Error(`The "${params.action}" action requires "${field}"`);
121
- }
122
- }
123
-
124
- return action.execute(params as any, config.createContext(ctx), signal, onUpdate);
125
- },
126
- });
127
- }
128
-
129
- /** A tuple type without its first element. */
130
- type DropFirst<T extends readonly unknown[]> = T extends readonly [unknown, ...infer R] ? R : never;
131
-
132
- /** A renderer's params after the leading args/result. */
133
- type RenderCallTail = DropFirst<Parameters<NonNullable<ToolDefinition["renderCall"]>>>;
134
- type RenderResultTail = DropFirst<Parameters<NonNullable<ToolDefinition["renderResult"]>>>;
135
-
136
- /**
137
- * Bash-style timing for a tool row: a live "Elapsed" ticker while running, a final "Took" once
138
- * settled. `renderCall`/`renderResult` wrap the built component in place of the args/result.
139
- */
140
- interface Timing {
141
- renderCall(inner: Component, ...rest: RenderCallTail): Component;
142
- renderResult(inner: Component, ...rest: RenderResultTail): Component;
143
- }
144
-
145
- /** Per-row timing state, kept in `ToolRenderContext.state` so the timing classes stay stateless. */
146
- interface TimingState {
147
- startedAt?: number;
148
- endedAt?: number;
149
- interval?: ReturnType<typeof setInterval>;
150
- hasResult?: boolean;
151
- }
152
-
153
- class ActiveTiming implements Timing {
154
- // No result yet: `renderCall` owns the live "Elapsed" ticker.
155
- renderCall(inner: Component, ...[theme, context]: RenderCallTail): Component {
156
- const state = context.state as TimingState;
157
-
158
- if (context.executionStarted && state.startedAt === undefined) {
159
- state.startedAt = Date.now();
160
- delete state.endedAt;
161
- delete state.hasResult;
162
- }
163
-
164
- // Not started, or final (`renderResult` owns the "Took" line): render nothing here.
165
- if (state.startedAt === undefined || !context.isPartial) return inner;
166
-
167
- state.interval ??= setInterval(() => context.invalidate(), 1000);
168
-
169
- // `renderResult` runs later in the same render pass, so decide visibility at render time:
170
- // hide once any result has arrived.
171
- return this.withTimingLine(inner, "Elapsed", Date.now() - state.startedAt, theme, () => !state.hasResult);
172
- }
173
-
174
- // A result exists: `renderResult` owns the line — "Elapsed" while streaming, "Took" once final.
175
- renderResult(inner: Component, ...[options, theme, context]: RenderResultTail): Component {
176
- const state = context.state as TimingState;
177
- state.hasResult = true;
178
-
179
- const isRunning = options.isPartial && !context.isError;
180
- if (!isRunning) this.settle(state);
181
- if (state.startedAt === undefined) return inner;
182
-
183
- const endTime = state.endedAt ?? Date.now();
184
- return this.withTimingLine(inner, isRunning ? "Elapsed" : "Took", endTime - state.startedAt, theme);
185
- }
186
-
187
- private settle(state: TimingState): void {
188
- if (state.startedAt !== undefined) {
189
- state.endedAt ??= Date.now();
190
- }
191
-
192
- if (state.interval) {
193
- clearInterval(state.interval);
194
- delete state.interval;
195
- }
196
- }
197
-
198
- private withTimingLine(content: Component, label: string, ms: number, theme: Theme, visible?: () => boolean): Component {
199
- const container = new Container();
200
- container.addChild(content);
201
-
202
- container.addChild(new Spacer(1));
203
- container.addChild(new Text(theme.fg("muted", `${label} ${formatDuration(ms)}`), 0, 0));
204
-
205
- if (!visible) return container;
206
-
207
- return {
208
- invalidate: () => container.invalidate(),
209
- render: (width) => (visible() ? container.render(width) : content.render(width)),
210
- };
211
- }
212
- }
213
-
214
- class NoTiming implements Timing {
215
- renderCall(inner: Component): Component {
216
- return inner;
217
- }
218
- renderResult(inner: Component): Component {
219
- return inner;
220
- }
221
- }