@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persist pi's startup model into its global settings file
|
|
3
|
+
* ($PI_CODING_AGENT_DIR/settings.json, default ~/.pi/agent/settings.json) —
|
|
4
|
+
* the same keys pi's own `/model` ctrl+s writes
|
|
5
|
+
* (settings-manager.d.ts setDefaultModelAndProvider/setDefaultThinkingLevel).
|
|
6
|
+
* Atomic: write temp file + rename. Preserves every other key.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export function piSettingsPath(env: NodeJS.ProcessEnv = process.env, home = homedir()): string {
|
|
14
|
+
const envDir = env.PI_CODING_AGENT_DIR?.trim();
|
|
15
|
+
return join(envDir !== undefined && envDir.length > 0 ? envDir : join(home, ".pi", "agent"), "settings.json");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function mergeDefaultModel(
|
|
19
|
+
raw: unknown,
|
|
20
|
+
patch: { provider: string; model: string; thinkingLevel?: string },
|
|
21
|
+
): Record<string, unknown> {
|
|
22
|
+
const settings = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? { ...(raw as Record<string, unknown>) } : {};
|
|
23
|
+
settings.defaultProvider = patch.provider;
|
|
24
|
+
settings.defaultModel = patch.model;
|
|
25
|
+
if (patch.thinkingLevel !== undefined) settings.defaultThinkingLevel = patch.thinkingLevel;
|
|
26
|
+
return settings;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** pi's saved startup model (`provider/id`) and thinking level, if any. */
|
|
30
|
+
export function readDefaultModel(deps: { readFile?: (p: string) => string; env?: NodeJS.ProcessEnv } = {}): { key?: string; thinking?: string } {
|
|
31
|
+
try {
|
|
32
|
+
const path = piSettingsPath(deps.env);
|
|
33
|
+
const raw = JSON.parse(deps.readFile !== undefined ? deps.readFile(path) : readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
34
|
+
const key = typeof raw.defaultProvider === "string" && typeof raw.defaultModel === "string" ? `${raw.defaultProvider}/${raw.defaultModel}` : undefined;
|
|
35
|
+
return { key, thinking: typeof raw.defaultThinkingLevel === "string" ? raw.defaultThinkingLevel : undefined };
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function persistDefaultModel(
|
|
42
|
+
patch: { provider: string; model: string; thinkingLevel?: string },
|
|
43
|
+
deps: { readFile?: (p: string) => string; env?: NodeJS.ProcessEnv } = {},
|
|
44
|
+
): string {
|
|
45
|
+
const path = piSettingsPath(deps.env);
|
|
46
|
+
let current: unknown = {};
|
|
47
|
+
try {
|
|
48
|
+
current = JSON.parse(deps.readFile !== undefined ? deps.readFile(path) : readFileSync(path, "utf8")) as unknown;
|
|
49
|
+
} catch {
|
|
50
|
+
current = {};
|
|
51
|
+
}
|
|
52
|
+
const next = mergeDefaultModel(current, patch);
|
|
53
|
+
const tmp = `${path}.tmp-${String(process.pid)}`;
|
|
54
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
55
|
+
renameSync(tmp, path);
|
|
56
|
+
return path;
|
|
57
|
+
}
|
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
*
|
|
31
31
|
* On the Fusion row, Tab cycles effort → lead → sidekick (Shift+Tab
|
|
32
32
|
* reverses); the lead/sidekick focus opens an inline dropdown fed by the
|
|
33
|
-
* preset lists.
|
|
33
|
+
* preset lists. With the effort control focused, Space toggles whether ←/→
|
|
34
|
+
* adjusts the lead's or the sidekick's effort (default: lead).
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
37
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
37
38
|
import { frameOverlay } from "@pi-unipi/core";
|
|
38
39
|
import {
|
|
39
40
|
effortLabel,
|
|
@@ -67,6 +68,8 @@ export interface PickerState {
|
|
|
67
68
|
active: ActiveSelection | undefined;
|
|
68
69
|
/** The session's current model — its row lights up with ✓. */
|
|
69
70
|
currentModelKey: ModelKey | undefined;
|
|
71
|
+
/** pi's persisted startup model — its row gets a dim `default` tag. */
|
|
72
|
+
defaultModelKey: ModelKey | undefined;
|
|
70
73
|
effort: Readonly<Record<ModelKey, EffortLevel>>;
|
|
71
74
|
/** Effort used for a model with no remembered level. */
|
|
72
75
|
fallbackEffort: EffortLevel;
|
|
@@ -98,6 +101,8 @@ export interface PickerOptions {
|
|
|
98
101
|
state: PickerState;
|
|
99
102
|
theme: PickerTheme;
|
|
100
103
|
onDone: (result: PickerResult) => void;
|
|
104
|
+
/** alt+enter: persist the highlighted row as the startup default (then apply). */
|
|
105
|
+
onSetDefault?: (result: PickerResult) => void;
|
|
101
106
|
onRenderRequest?: (() => void) | undefined;
|
|
102
107
|
/** Rows visible in the list window. */
|
|
103
108
|
visibleRows?: number | undefined;
|
|
@@ -171,6 +176,7 @@ function pad(text: string, width: number): string {
|
|
|
171
176
|
export class ModelPicker {
|
|
172
177
|
private readonly theme: PickerTheme;
|
|
173
178
|
private readonly onDone: (result: PickerResult) => void;
|
|
179
|
+
private readonly onSetDefault?: (result: PickerResult) => void;
|
|
174
180
|
private readonly onRenderRequest: (() => void) | undefined;
|
|
175
181
|
private readonly visibleRows: number;
|
|
176
182
|
private readonly modelsByKey: Map<ModelKey, PickerModel>;
|
|
@@ -189,6 +195,8 @@ export class ModelPicker {
|
|
|
189
195
|
private search = "";
|
|
190
196
|
private selected = 0;
|
|
191
197
|
private focus: FusionFocus = "effort";
|
|
198
|
+
/** Which side ←/→ adjusts on the Fusion row's effort control. */
|
|
199
|
+
private effortTarget: "lead" | "sidekick" = "lead";
|
|
192
200
|
private dropdownIndex = 0;
|
|
193
201
|
private done = false;
|
|
194
202
|
|
|
@@ -196,6 +204,7 @@ export class ModelPicker {
|
|
|
196
204
|
this.state = options.state;
|
|
197
205
|
this.theme = options.theme;
|
|
198
206
|
this.onDone = options.onDone;
|
|
207
|
+
this.onSetDefault = options.onSetDefault;
|
|
199
208
|
this.onRenderRequest = options.onRenderRequest;
|
|
200
209
|
this.visibleRows = options.visibleRows ?? DEFAULT_VISIBLE_ROWS;
|
|
201
210
|
this.modelsByKey = new Map(options.state.models.map((m) => [m.key, m]));
|
|
@@ -209,14 +218,21 @@ export class ModelPicker {
|
|
|
209
218
|
this.priceRange = { min: prices.length > 0 ? Math.min(...prices) : 0, max: prices.length > 0 ? Math.max(...prices) : 0 };
|
|
210
219
|
this.effort = { ...options.state.effort };
|
|
211
220
|
const active = options.state.active;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
options.state.
|
|
221
|
+
// Saved keys can go stale (provider renamed a model, logged out); only
|
|
222
|
+
// accept ones still in the catalogue, else the Fusion row looks ready but
|
|
223
|
+
// cannot be applied.
|
|
224
|
+
const known = (...keys: (ModelKey | undefined)[]): ModelKey | undefined =>
|
|
225
|
+
keys.find((k) => k !== undefined && this.modelsByKey.has(k));
|
|
226
|
+
this.lead = known(
|
|
227
|
+
active?.kind === "fusion" ? active.lead : undefined,
|
|
228
|
+
options.state.fusionDefault.lead,
|
|
229
|
+
...options.state.fusionLeads,
|
|
230
|
+
);
|
|
231
|
+
this.sidekick = known(
|
|
232
|
+
active?.kind === "fusion" ? active.sidekick : undefined,
|
|
233
|
+
options.state.fusionDefault.sidekick,
|
|
234
|
+
...options.state.fusionSidekicks,
|
|
235
|
+
);
|
|
220
236
|
this.fusionLeadEffort =
|
|
221
237
|
(active?.kind === "fusion" ? active.leadEffort : undefined) ??
|
|
222
238
|
(this.lead !== undefined ? this.effort[this.lead] : undefined) ??
|
|
@@ -266,8 +282,9 @@ export class ModelPicker {
|
|
|
266
282
|
...this.state.fusionLeads,
|
|
267
283
|
...this.state.fusionSidekicks,
|
|
268
284
|
];
|
|
269
|
-
// The
|
|
270
|
-
|
|
285
|
+
// The preset IS the list; the rest of the catalogue appears only while
|
|
286
|
+
// searching (or when nothing is curated yet, so Fusion stays usable).
|
|
287
|
+
if (this.search.length > 0 || !this.hasPreset()) ordered.push(...this.state.models.map((m) => m.key));
|
|
271
288
|
for (const key of ordered) {
|
|
272
289
|
if (seen.has(key) || !this.modelsByKey.has(key)) continue;
|
|
273
290
|
if (!this.matchesSearch(key)) continue;
|
|
@@ -286,9 +303,27 @@ export class ModelPicker {
|
|
|
286
303
|
return this.rows()[this.selected];
|
|
287
304
|
}
|
|
288
305
|
|
|
289
|
-
|
|
306
|
+
/** Whether any lead/sidekick is curated (and still in the catalogue). */
|
|
307
|
+
private hasPreset(): boolean {
|
|
308
|
+
return [...this.state.fusionLeads, ...this.state.fusionSidekicks].some((k) => this.modelsByKey.has(k));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Dropdown entries: only the curated list for this side. Typing searches
|
|
312
|
+
* the whole catalogue (curated matches first, a dim separator, the rest);
|
|
313
|
+
* an empty list offers the whole catalogue (Fusion works uncurated). */
|
|
314
|
+
private dropdownEntries(): Array<ModelKey | "sep"> {
|
|
290
315
|
const source = this.focus === "lead" ? this.state.fusionLeads : this.state.fusionSidekicks;
|
|
291
|
-
|
|
316
|
+
const listed = source.filter((k) => this.modelsByKey.has(k));
|
|
317
|
+
const curated = listed.filter((k) => this.matchesSearch(k));
|
|
318
|
+
if (listed.length > 0 && this.search.length === 0) return curated;
|
|
319
|
+
const rest = this.state.models.map((m) => m.key).filter((k) => !curated.includes(k) && this.matchesSearch(k));
|
|
320
|
+
if (curated.length === 0) return rest;
|
|
321
|
+
if (rest.length === 0) return curated;
|
|
322
|
+
return [...curated, "sep", ...rest];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private dropdownItems(): ModelKey[] {
|
|
326
|
+
return this.dropdownEntries().filter((e): e is ModelKey => e !== "sep");
|
|
292
327
|
}
|
|
293
328
|
|
|
294
329
|
// ── Input ────────────────────────────────────────────────────────────────
|
|
@@ -301,6 +336,7 @@ export class ModelPicker {
|
|
|
301
336
|
if (matchesKey(data, Key.escape)) {
|
|
302
337
|
if (inDropdown) {
|
|
303
338
|
this.focus = "effort";
|
|
339
|
+
this.search = "";
|
|
304
340
|
this.changed();
|
|
305
341
|
return;
|
|
306
342
|
}
|
|
@@ -309,15 +345,14 @@ export class ModelPicker {
|
|
|
309
345
|
}
|
|
310
346
|
|
|
311
347
|
const disabledFusion = row?.kind === "fusion" && !this.fusionAvailable();
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
(matchesKey(data, Key.tab) ||
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return;
|
|
348
|
+
if (disabledFusion && !inDropdown) {
|
|
349
|
+
// Incomplete pair: Enter/Tab open the dropdown for the missing half.
|
|
350
|
+
if (matchesKey(data, Key.tab) || matchesKey(data, Key.enter) || data === "\r") {
|
|
351
|
+
this.openMissing();
|
|
352
|
+
this.changed();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (matchesKey(data, Key.left) || matchesKey(data, Key.right) || matchesKey(data, "alt+enter")) return;
|
|
321
356
|
}
|
|
322
357
|
|
|
323
358
|
if (inDropdown) {
|
|
@@ -329,11 +364,29 @@ export class ModelPicker {
|
|
|
329
364
|
} else if (matchesKey(data, Key.tab)) {
|
|
330
365
|
this.applyDropdown(items);
|
|
331
366
|
this.cycleFocus(matchesKey(data, "shift+tab"));
|
|
367
|
+
} else if (matchesKey(data, "alt+enter")) {
|
|
368
|
+
this.applyDropdown(items);
|
|
369
|
+
if (!this.fusionAvailable()) this.openMissing();
|
|
370
|
+
else {
|
|
371
|
+
this.focus = "effort";
|
|
372
|
+
this.setDefault(row);
|
|
373
|
+
this.confirm(row);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
332
376
|
} else if (matchesKey(data, Key.enter) || data === "\r") {
|
|
333
377
|
this.applyDropdown(items);
|
|
334
|
-
this.
|
|
378
|
+
this.search = "";
|
|
379
|
+
if (this.fusionAvailable()) this.focus = "effort";
|
|
380
|
+
else this.openMissing();
|
|
381
|
+
} else if (matchesKey(data, Key.backspace) || data === "\x7f") {
|
|
382
|
+
// Typing in a dropdown searches the whole catalogue.
|
|
383
|
+
this.search = this.search.slice(0, -1);
|
|
384
|
+
this.dropdownIndex = 0;
|
|
335
385
|
} else {
|
|
336
|
-
|
|
386
|
+
const ch = printable(data);
|
|
387
|
+
if (ch === undefined || ch === " ") return;
|
|
388
|
+
this.search += ch;
|
|
389
|
+
this.dropdownIndex = 0;
|
|
337
390
|
}
|
|
338
391
|
this.changed();
|
|
339
392
|
return;
|
|
@@ -351,7 +404,11 @@ export class ModelPicker {
|
|
|
351
404
|
const delta: -1 | 1 = matchesKey(data, Key.left) ? -1 : 1;
|
|
352
405
|
if (row?.kind === "fusion") {
|
|
353
406
|
// Fusion-row effort is its own state: never touches per-model memory.
|
|
354
|
-
|
|
407
|
+
// Space picks which side ←/→ adjusts.
|
|
408
|
+
if (this.focus === "effort") {
|
|
409
|
+
if (this.effortTarget === "lead") this.fusionLeadEffort = stepEffort(this.fusionLeadEffort, delta);
|
|
410
|
+
else this.fusionSidekickEffort = stepEffort(this.fusionSidekickEffort, delta);
|
|
411
|
+
}
|
|
355
412
|
} else if (row?.key !== undefined) {
|
|
356
413
|
this.effort[row.key] = stepEffort(this.effortFor(row.key), delta);
|
|
357
414
|
}
|
|
@@ -364,13 +421,27 @@ export class ModelPicker {
|
|
|
364
421
|
return;
|
|
365
422
|
}
|
|
366
423
|
} else if (data === " " || matchesKey(data, Key.space)) {
|
|
367
|
-
// Space = quick action
|
|
424
|
+
// Space = quick action. On a model row it stages the highlighted model
|
|
425
|
+
// as the Fusion lead; on the Fusion row it toggles which side the
|
|
426
|
+
// effort control adjusts (Tab is taken by the lead/sidekick cycle).
|
|
427
|
+
if (row?.kind === "fusion") {
|
|
428
|
+
if (this.focus === "effort" && this.fusionAvailable()) {
|
|
429
|
+
this.effortTarget = this.effortTarget === "lead" ? "sidekick" : "lead";
|
|
430
|
+
this.changed();
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
368
434
|
if (row?.kind === "model" && !disabledFusion) {
|
|
369
435
|
this.lead = row.key;
|
|
370
436
|
this.changed();
|
|
371
437
|
return;
|
|
372
438
|
}
|
|
373
439
|
return;
|
|
440
|
+
} else if (matchesKey(data, "alt+enter")) {
|
|
441
|
+
// Alt+Enter = apply AND keep it as the startup default.
|
|
442
|
+
this.setDefault(row);
|
|
443
|
+
this.confirm(row);
|
|
444
|
+
return;
|
|
374
445
|
} else if (matchesKey(data, Key.enter) || data === "\r") {
|
|
375
446
|
this.confirm(row);
|
|
376
447
|
return;
|
|
@@ -399,6 +470,12 @@ export class ModelPicker {
|
|
|
399
470
|
}
|
|
400
471
|
}
|
|
401
472
|
|
|
473
|
+
/** Open the lead (or, if the lead is set, the sidekick) dropdown. */
|
|
474
|
+
private openMissing(): void {
|
|
475
|
+
this.focus = this.lead === undefined ? "effort" : "lead"; // cycleFocus steps one past this
|
|
476
|
+
this.cycleFocus();
|
|
477
|
+
}
|
|
478
|
+
|
|
402
479
|
private applyDropdown(items: ModelKey[]): void {
|
|
403
480
|
const pick = items[this.dropdownIndex];
|
|
404
481
|
if (pick === undefined) return;
|
|
@@ -406,6 +483,31 @@ export class ModelPicker {
|
|
|
406
483
|
else this.sidekick = pick;
|
|
407
484
|
}
|
|
408
485
|
|
|
486
|
+
private setDefault(row: Row | undefined): void {
|
|
487
|
+
if (row === undefined || this.onSetDefault === undefined) return;
|
|
488
|
+
const mark = row.kind === "fusion" ? this.lead : row.key;
|
|
489
|
+
if (mark !== undefined) this.state.defaultModelKey = mark;
|
|
490
|
+
this.changed();
|
|
491
|
+
if (row.kind === "fusion") {
|
|
492
|
+
if (this.lead === undefined || this.sidekick === undefined) return;
|
|
493
|
+
this.onSetDefault({
|
|
494
|
+
type: "fusion",
|
|
495
|
+
lead: this.lead,
|
|
496
|
+
sidekick: this.sidekick,
|
|
497
|
+
leadEffort: this.fusionLeadEffort,
|
|
498
|
+
sidekickEffort: this.fusionSidekickEffort,
|
|
499
|
+
effortMap: { ...this.effort },
|
|
500
|
+
});
|
|
501
|
+
} else if (row.key !== undefined) {
|
|
502
|
+
this.onSetDefault({
|
|
503
|
+
type: "single",
|
|
504
|
+
model: row.key,
|
|
505
|
+
effort: this.effortFor(row.key),
|
|
506
|
+
effortMap: { ...this.effort },
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
409
511
|
private confirm(row: Row | undefined): void {
|
|
410
512
|
if (row === undefined) return;
|
|
411
513
|
if (row.kind === "fusion") {
|
|
@@ -507,11 +609,16 @@ export class ModelPicker {
|
|
|
507
609
|
: highlighted
|
|
508
610
|
? t.fg("accent", nameRaw)
|
|
509
611
|
: t.fg("text", nameRaw);
|
|
510
|
-
const name = `${providerPrefix}${styled}`;
|
|
612
|
+
const name = `${providerPrefix}${styled}${row.kind === "model" && row.key === this.state.defaultModelKey ? ` ${t.fg("dim", "default")}` : ""}`;
|
|
511
613
|
const badge = model?.badge;
|
|
512
614
|
const badgeGlyph = badge === undefined ? "" : ` ${t.fg(badge === "new" ? "success" : badge === "promotion" ? "accent" : "warning", "✱")}`;
|
|
513
615
|
|
|
514
|
-
const level =
|
|
616
|
+
const level =
|
|
617
|
+
row.kind === "fusion"
|
|
618
|
+
? this.effortTarget === "lead"
|
|
619
|
+
? this.fusionLeadEffort
|
|
620
|
+
: this.fusionSidekickEffort
|
|
621
|
+
: this.effortFor(row.key);
|
|
515
622
|
const arrowsOn = !disabledFusion && highlighted && this.focus === "effort";
|
|
516
623
|
const left = arrowsOn ? t.fg("accent", "←") : " ";
|
|
517
624
|
const right = arrowsOn ? t.fg("accent", "→") : " ";
|
|
@@ -524,19 +631,24 @@ export class ModelPicker {
|
|
|
524
631
|
let line = `${pointer} ${marker} ${pad(`${name}${badgeGlyph}`, nameCol)} ${left} ${this.bar(level, !disabledFusion && highlighted)} ${right} ${pad(label, 8)}`;
|
|
525
632
|
|
|
526
633
|
if (row.kind === "fusion") {
|
|
527
|
-
if (disabledFusion) {
|
|
528
|
-
line += ` ${t.fg("dim", "not configured — open /unipi:settings → Fusion → Edit fusion presets…")}`;
|
|
529
|
-
} else {
|
|
634
|
+
if (!disabledFusion) {
|
|
530
635
|
const leadName = this.nameOf(this.lead, 14);
|
|
531
636
|
const sideName = this.nameOf(this.sidekick, 14);
|
|
532
637
|
const leadFocused = highlighted && this.focus === "lead";
|
|
533
638
|
const sideFocused = highlighted && this.focus === "sidekick";
|
|
639
|
+
// Each side shows its effort; a ▸ marks the side ←/→ adjusts.
|
|
640
|
+
const effortSuffix = (side: "lead" | "sidekick") => {
|
|
641
|
+
const lv = effortLabel(side === "lead" ? this.fusionLeadEffort : this.fusionSidekickEffort);
|
|
642
|
+
return highlighted && this.focus === "effort" && this.effortTarget === side
|
|
643
|
+
? t.fg("accent", t.bold(` ▸${lv}`))
|
|
644
|
+
: t.fg("dim", ` ${lv}`);
|
|
645
|
+
};
|
|
534
646
|
const leadText = leadFocused
|
|
535
|
-
? `${t.fg("accent", t.bold("Lead"))} ${t.fg("accent", leadName)} ${t.fg("accent", "▾")}`
|
|
536
|
-
: `${t.fg("dim", "Lead")} ${t.fg("text", leadName)} ${t.fg("dim", "▾")}`;
|
|
647
|
+
? `${t.fg("accent", t.bold("Lead"))} ${t.fg("accent", leadName)} ${t.fg("accent", "▾")}${effortSuffix("lead")}`
|
|
648
|
+
: `${t.fg("dim", "Lead")} ${t.fg("text", leadName)} ${t.fg("dim", "▾")}${effortSuffix("lead")}`;
|
|
537
649
|
const sideText = sideFocused
|
|
538
|
-
? `${t.fg("accent", t.bold("Sidekick"))} ${t.fg("accent", sideName)} ${t.fg("accent", "▾")}`
|
|
539
|
-
: `${t.fg("dim", "Sidekick")} ${t.fg("text", sideName)} ${t.fg("dim", "▾")}`;
|
|
650
|
+
? `${t.fg("accent", t.bold("Sidekick"))} ${t.fg("accent", sideName)} ${t.fg("accent", "▾")}${effortSuffix("sidekick")}`
|
|
651
|
+
: `${t.fg("dim", "Sidekick")} ${t.fg("text", sideName)} ${t.fg("dim", "▾")}${effortSuffix("sidekick")}`;
|
|
540
652
|
line += ` ${leadText} ${sideText}`;
|
|
541
653
|
}
|
|
542
654
|
}
|
|
@@ -545,23 +657,37 @@ export class ModelPicker {
|
|
|
545
657
|
|
|
546
658
|
private renderDropdown(width: number, nameCol: number): string[] {
|
|
547
659
|
const t = this.theme;
|
|
660
|
+
const entries = this.dropdownEntries();
|
|
548
661
|
const items = this.dropdownItems();
|
|
549
662
|
const indent = " ".repeat(MARKER_COL + nameCol + 5);
|
|
550
663
|
if (items.length === 0) {
|
|
551
|
-
return [`${indent}${t.fg("warning",
|
|
664
|
+
return [`${indent}${t.fg("warning", "no matching models")}`];
|
|
552
665
|
}
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
666
|
+
// Selectable index ↔ entry index mapping (a "sep" row renders but can't be picked).
|
|
667
|
+
const selectableIdx = new Map<ModelKey, number>();
|
|
668
|
+
items.forEach((key, i) => selectableIdx.set(key, i));
|
|
669
|
+
const firstEntryOfSel = items.map((key) => entries.indexOf(key));
|
|
670
|
+
const win = 8;
|
|
671
|
+
const selStart = Math.max(0, Math.min(this.dropdownIndex - Math.floor(win / 2), items.length - win));
|
|
672
|
+
const sliceStart = firstEntryOfSel[selStart] ?? 0;
|
|
673
|
+
const slice = entries.slice(sliceStart, sliceStart + win);
|
|
674
|
+
const above = sliceStart;
|
|
675
|
+
const below = Math.max(0, entries.length - (sliceStart + slice.length));
|
|
676
|
+
const more = (n: number, dir: string) => truncateToWidth(`${indent}${t.fg("dim", `${dir} ${String(n)} more`)}`, Math.max(1, width - 1));
|
|
677
|
+
return [
|
|
678
|
+
...(above > 0 ? [more(above, "↑")] : []),
|
|
679
|
+
...slice.map((entry) => {
|
|
680
|
+
if (entry === "sep") return truncateToWidth(`${indent}${t.fg("dim", "── all models ──")}`, Math.max(1, width - 1));
|
|
681
|
+
const selIdx = selectableIdx.get(entry) ?? 0;
|
|
682
|
+
const isCur = selIdx === this.dropdownIndex;
|
|
683
|
+
const isSet = entry === (this.focus === "lead" ? this.lead : this.sidekick);
|
|
560
684
|
const glyph = isCur ? t.fg("accent", "▸") : " ";
|
|
561
|
-
const label = isCur ? t.fg("accent", t.bold(this.nameOf(
|
|
685
|
+
const label = isCur ? t.fg("accent", t.bold(this.nameOf(entry, 28))) : t.fg("text", this.nameOf(entry, 28));
|
|
562
686
|
const star = isSet ? t.fg("dim", " *") : "";
|
|
563
687
|
return truncateToWidth(`${indent}${glyph} ${label}${star}`, Math.max(1, width - 1));
|
|
564
|
-
|
|
688
|
+
}),
|
|
689
|
+
...(below > 0 ? [more(below, "↓")] : []),
|
|
690
|
+
];
|
|
565
691
|
}
|
|
566
692
|
|
|
567
693
|
private renderPricePanel(row: Row | undefined, width: number): string[] {
|
|
@@ -625,7 +751,9 @@ export class ModelPicker {
|
|
|
625
751
|
const keyText = keys.map((k) => t.fg("text", k)).join(t.fg("dim", " · "));
|
|
626
752
|
out.push(truncateToWidth(` ${t.fg("dim", "Model key")} ${keyText}`, width - 1));
|
|
627
753
|
}
|
|
628
|
-
|
|
754
|
+
for (const line of wrapTextWithAnsi(` ${description}`, Math.max(1, width - 1)).slice(0, 3)) {
|
|
755
|
+
out.push(truncateToWidth(line, width - 1));
|
|
756
|
+
}
|
|
629
757
|
return out;
|
|
630
758
|
}
|
|
631
759
|
|
|
@@ -633,13 +761,17 @@ export class ModelPicker {
|
|
|
633
761
|
const t = this.theme;
|
|
634
762
|
const parts: string[] = [];
|
|
635
763
|
if (row?.kind === "fusion" && !this.fusionAvailable()) {
|
|
636
|
-
parts.push("↑↓ select", "esc cancel");
|
|
764
|
+
parts.push("↑↓ select", `enter pick ${this.lead === undefined ? "lead" : "sidekick"}`, "esc cancel");
|
|
637
765
|
} else if (row?.kind === "fusion" && this.focus !== "effort") {
|
|
638
|
-
parts.push("↑↓ select", `tab ${this.focus === "lead" ? "sidekick" : "effort"}`, "enter apply", "esc collapse");
|
|
766
|
+
parts.push("↑↓ select", "type to search all models", `tab ${this.focus === "lead" ? "sidekick" : "effort"}`, "enter apply", "alt+enter set default", "esc collapse");
|
|
639
767
|
} else {
|
|
640
768
|
parts.push("↑↓ select");
|
|
641
|
-
if (row?.kind === "fusion")
|
|
642
|
-
|
|
769
|
+
if (row?.kind === "fusion") {
|
|
770
|
+
const other = this.effortTarget === "lead" ? "sidekick" : "lead";
|
|
771
|
+
parts.push("tab lead", `←→ ${this.effortTarget} effort`, `space → ${other}`, "enter confirm", "alt+enter set default", "esc cancel");
|
|
772
|
+
} else {
|
|
773
|
+
parts.push("←→ effort", "enter/tab confirm", "alt+enter set default", "space set lead", "esc cancel");
|
|
774
|
+
}
|
|
643
775
|
}
|
|
644
776
|
return t.fg("dim", parts.join(" · "));
|
|
645
777
|
}
|
|
@@ -675,6 +807,9 @@ export class ModelPicker {
|
|
|
675
807
|
if (r === undefined) continue;
|
|
676
808
|
const highlighted = i === this.selected;
|
|
677
809
|
lines.push(this.renderRow(r, highlighted, width, nameCol));
|
|
810
|
+
if (r.kind === "fusion" && !this.fusionAvailable()) {
|
|
811
|
+
lines.push(truncateToWidth(` ${t.fg("dim", "pick a lead and sidekick — the catalogue below works too (open /unipi:settings → Fusion → Edit fusion presets… to curate)")}`, Math.max(1, width - 1)));
|
|
812
|
+
}
|
|
678
813
|
if (highlighted && r.kind === "fusion" && this.focus !== "effort") {
|
|
679
814
|
lines.push(...this.renderDropdown(width, nameCol));
|
|
680
815
|
}
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
18
18
|
import { homedir } from "node:os";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
|
-
import { getSettings, registerSettings, setSettings } from "@pi-unipi/core";
|
|
20
|
+
import { getSettings, getSettingsScoped, registerSettings, setSettings } from "@pi-unipi/core";
|
|
21
|
+
import { persistDefaultModel, readDefaultModel } from "./pi-settings.js";
|
|
21
22
|
|
|
22
23
|
export const PRESET_SCHEMA_VERSION = 1;
|
|
23
24
|
|
|
@@ -206,6 +207,48 @@ export interface LoadedPreset {
|
|
|
206
207
|
hasProjectLayer: boolean;
|
|
207
208
|
}
|
|
208
209
|
|
|
210
|
+
const STARTUP_THINKING = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Hub → pi: a global write of `startup.*` becomes pi's own startup default
|
|
214
|
+
* (settings.json defaultProvider/defaultModel/defaultThinkingLevel — what
|
|
215
|
+
* pi's /model ctrl+s and the picker's alt+enter write). A single startup
|
|
216
|
+
* model also stops a remembered Fusion pair from overriding it at startup.
|
|
217
|
+
* Exported for tests.
|
|
218
|
+
*/
|
|
219
|
+
export function applyStartupSettings(layer: Record<string, unknown>, home = homedir()): boolean {
|
|
220
|
+
const startup = layer.startup as { model?: unknown; thinking?: unknown } | undefined;
|
|
221
|
+
const model = typeof startup?.model === "string" ? startup.model.trim() : "";
|
|
222
|
+
const thinking = typeof startup?.thinking === "string" && (STARTUP_THINKING as readonly string[]).includes(startup.thinking) ? startup.thinking : undefined;
|
|
223
|
+
const parts = model ? splitModelKey(model) : undefined;
|
|
224
|
+
const current = readDefaultModel();
|
|
225
|
+
if (parts === undefined && thinking === undefined) return false;
|
|
226
|
+
const nextKey = parts ? model : current.key;
|
|
227
|
+
if (nextKey === current.key && (thinking === undefined || thinking === current.thinking)) return false; // mirror echo
|
|
228
|
+
const target = nextKey ? splitModelKey(nextKey) : undefined;
|
|
229
|
+
if (target === undefined) return false;
|
|
230
|
+
persistDefaultModel({ provider: target.provider, model: target.id, thinkingLevel: thinking });
|
|
231
|
+
if (parts !== undefined) {
|
|
232
|
+
const path = globalPresetPath(home);
|
|
233
|
+
const existing = mergePresets(emptyPreset(), parsePreset(readJson(path)));
|
|
234
|
+
if (existing.active?.kind === "fusion" && existing.active.lead !== model) {
|
|
235
|
+
saveRuntimeState(path, { effort: existing.effort, recent: existing.recent, active: { kind: "single", model } });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** pi → hub: show pi's real startup default in /unipi:settings (it also
|
|
242
|
+
* changes through pi's /model ctrl+s and the picker's alt+enter). */
|
|
243
|
+
export function mirrorStartupFromPi(cwd: string): void {
|
|
244
|
+
const { key, thinking } = readDefaultModel();
|
|
245
|
+
const shown = (getSettingsScoped("fusion", "global", cwd)?.startup ?? {}) as { model?: string; thinking?: string };
|
|
246
|
+
const patch: Record<string, string> = {};
|
|
247
|
+
if (key !== undefined && shown.model !== key) patch.model = key;
|
|
248
|
+
if (thinking !== undefined && shown.thinking !== thinking && (STARTUP_THINKING as readonly string[]).includes(thinking)) patch.thinking = thinking;
|
|
249
|
+
if (Object.keys(patch).length > 0) setSettings("fusion", { startup: patch }, "global", cwd);
|
|
250
|
+
}
|
|
251
|
+
|
|
209
252
|
// Registered with the unified settings hub: the DEFAULT PAIR is editable
|
|
210
253
|
// there (model pickers, engine-layered). The curated arrays, effort, recent,
|
|
211
254
|
// badges and prices remain in the preset files — the hub action "Edit fusion presets…" owns
|
|
@@ -213,8 +256,19 @@ export interface LoadedPreset {
|
|
|
213
256
|
registerSettings({
|
|
214
257
|
namespace: "fusion",
|
|
215
258
|
label: "Fusion",
|
|
216
|
-
defaults: { default: { lead: "", sidekick: "" } },
|
|
259
|
+
defaults: { default: { lead: "", sidekick: "" }, startup: { model: "" } },
|
|
260
|
+
onSet: (layer, scope) => {
|
|
261
|
+
if (scope === "global") applyStartupSettings(layer);
|
|
262
|
+
},
|
|
217
263
|
schema: [
|
|
264
|
+
{
|
|
265
|
+
title: "Startup",
|
|
266
|
+
description: "The model pi opens with. /unipi:model → alt+enter sets the same thing (on the Fusion row it starts in Fusion).",
|
|
267
|
+
fields: [
|
|
268
|
+
{ key: "startup.model", type: "model", label: "Startup model", capability: "text", emptyLabel: "pi default", scopes: ["global"] },
|
|
269
|
+
{ key: "startup.thinking", type: "enum", label: "Startup thinking level", options: [...STARTUP_THINKING], scopes: ["global"] },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
218
272
|
{
|
|
219
273
|
title: "Default pair",
|
|
220
274
|
description: "Used when the Fusion row is confirmed without editing",
|
|
@@ -52,7 +52,7 @@ You have a \`sidekick\` tool: a persistent subagent that works alongside you on
|
|
|
52
52
|
- The sidekick remembers everything from previous handoffs (code it wrote, files it explored, your earlier instructions), so don't re-explain context it already has. Its runtime state also persists: background shells and processes it started (dev servers, DB connections, long-running commands) usually survive between handoffs. Every brief that involves servers or long-running processes must say what to do with them — e.g. "the server from the previous handoff may still be running; check and reuse it, restart only if it's gone or the code changed" — and, when a later handoff may need them, tell it to leave them running.
|
|
53
53
|
- On each handoff give it: the goal, your plan, the constraints, the relevant files, and how to verify. Settle the consequential choices before handing off: the exact interface (signature, types, data shape, which existing helper to use) and the exact tests (cases, assertions, where to stub). Don't leave alternatives for it to pick; it will guess, and a wrong guess costs a whole extra round. A code snippet is fine when it is the clearest way to say it.
|
|
54
54
|
- **Never make the sidekick redo work you already did.** Results you already derived go into the brief as settled inputs (the values, or the path to the file holding them), not as an invitation to recompute. Ask for re-derivation only when you have reason to doubt them.
|
|
55
|
-
- Blocking dispatch is the default: a \`sidekick\` call waits and returns the report. Pass \`block: false\` only when you genuinely have parallel lead work to do meanwhile; don't start work redundant with what the sidekick is doing, and never poll \`read_subagent\` in a loop. When you have run out of parallel work and still need the report, wait with \`read_subagent\` (\`block: true\`) rather than ending your turn or guessing.
|
|
55
|
+
- Blocking dispatch is the default: a \`sidekick\` call waits and returns the report. Pass \`block: false\` only when you genuinely have parallel lead work to do meanwhile; don't start work redundant with what the sidekick is doing, and never poll \`read_subagent\` in a loop. When you have run out of parallel work and still need the report, wait with \`read_subagent\` (\`block: true\`) rather than ending your turn or guessing. A background handoff cannot ask the user for approval: any tool call that would need approval is automatically denied, so run approval-needing work with a blocking handoff.
|
|
56
56
|
- Calling \`sidekick\` again while a handoff is running injects the new message into that handoff as an interrupt — it never starts a second sidekick. Use it to redirect with a corrected brief, tell it to wrap up and report what it has, or send a purely informational update.
|
|
57
57
|
- If the user sends a message while a handoff is in flight, act on it before going back to waiting: handle lead-only work yourself, and for anything that concerns the running handoff, send an interrupt. Resume waiting only once you have deliberately decided the message changes nothing for the sidekick.
|
|
58
58
|
- Track the lead-only actions you have promised the user (messages, review replies, commit/PR updates) — the sidekick cannot do these, so a brief that includes one silently drops it. Do each one the moment the handoff returns, before dispatching a follow-up.
|