@causyne/agent 0.0.0-stage → 1.10.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.
- package/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
package/src/tui/theme.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// Terminal theme — colour that adapts instead of assuming.
|
|
2
|
+
//
|
|
3
|
+
// The CLI used hardcoded \x1b[36m-style escapes everywhere. That assumes a dark
|
|
4
|
+
// background, assumes colour is wanted, and assumes the terminal supports it.
|
|
5
|
+
// On a light terminal the "muted grey" comments were nearly invisible; piped
|
|
6
|
+
// into a file or a CI log, every line carried escape bytes.
|
|
7
|
+
//
|
|
8
|
+
// This module answers three questions once, at import: may we colour at all,
|
|
9
|
+
// how many colours, and is the background light or dark. Everything else in the
|
|
10
|
+
// TUI asks for a SEMANTIC role (accent, danger, muted) and gets whatever that
|
|
11
|
+
// resolves to here.
|
|
12
|
+
//
|
|
13
|
+
// Pure Node: no dependency, no shell. Readable by construction.
|
|
14
|
+
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Capability detection
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function detectColorSupport() {
|
|
22
|
+
// https://no-color.org — an explicit user opt-out beats every heuristic.
|
|
23
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return 0;
|
|
24
|
+
if (process.env.CAUSYNE_NO_COLOR === '1') return 0;
|
|
25
|
+
|
|
26
|
+
const forced = process.env.FORCE_COLOR;
|
|
27
|
+
if (forced === '0') return 0;
|
|
28
|
+
if (forced === '1') return 16;
|
|
29
|
+
if (forced === '2') return 256;
|
|
30
|
+
if (forced === '3') return 16777216;
|
|
31
|
+
|
|
32
|
+
// Not a terminal: piped to a file, a pager or a CI log. Escapes there are
|
|
33
|
+
// noise at best and corrupt parsing at worst.
|
|
34
|
+
if (!process.stdout.isTTY) return 0;
|
|
35
|
+
if (process.env.TERM === 'dumb') return 0;
|
|
36
|
+
|
|
37
|
+
const colorterm = String(process.env.COLORTERM || '').toLowerCase();
|
|
38
|
+
if (colorterm === 'truecolor' || colorterm === '24bit') return 16777216;
|
|
39
|
+
|
|
40
|
+
const term = String(process.env.TERM || '');
|
|
41
|
+
if (/-256(color)?$/i.test(term)) return 256;
|
|
42
|
+
if (/^(screen|xterm|vt100|vt220|rxvt|color|ansi|cygwin|linux)/i.test(term)) return 16;
|
|
43
|
+
|
|
44
|
+
// Windows Terminal and modern conhost handle truecolor; older ones do not.
|
|
45
|
+
if (os.platform() === 'win32') return 256;
|
|
46
|
+
return 16;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Is the terminal background light?
|
|
51
|
+
*
|
|
52
|
+
* COLORFGBG is set by several terminals as "foreground;background" in xterm
|
|
53
|
+
* palette indices. It is the only broadly available signal that does not
|
|
54
|
+
* require querying the terminal and parsing an escape reply, which would mean
|
|
55
|
+
* writing to stdout during startup.
|
|
56
|
+
*/
|
|
57
|
+
function detectLightBackground() {
|
|
58
|
+
const explicit = String(process.env.CAUSYNE_THEME || '').toLowerCase();
|
|
59
|
+
if (explicit === 'light') return true;
|
|
60
|
+
if (explicit === 'dark') return false;
|
|
61
|
+
|
|
62
|
+
const colorfgbg = process.env.COLORFGBG;
|
|
63
|
+
if (colorfgbg && colorfgbg.length <= 64) {
|
|
64
|
+
const parts = colorfgbg.split(';');
|
|
65
|
+
const background = parseInt(parts[parts.length - 1], 10);
|
|
66
|
+
if (Number.isFinite(background)) {
|
|
67
|
+
// 7 and 15 are the light greys/white of the base 16; 232-255 is the
|
|
68
|
+
// greyscale ramp, where the upper half reads as a light background.
|
|
69
|
+
if (background <= 15) return background === 7 || background === 15;
|
|
70
|
+
if (background >= 232) return background >= 244;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false; // dark is the safer default: most developer terminals are dark
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const COLOR_DEPTH = detectColorSupport();
|
|
77
|
+
export const IS_LIGHT = detectLightBackground();
|
|
78
|
+
export const HAS_COLOR = COLOR_DEPTH > 0;
|
|
79
|
+
|
|
80
|
+
// Unicode box drawing is not safe everywhere — Windows code pages and some CI
|
|
81
|
+
// runners mangle it. One flag decides, so every renderer stays consistent.
|
|
82
|
+
export const HAS_UNICODE = HAS_COLOR
|
|
83
|
+
&& process.env.CAUSYNE_ASCII !== '1'
|
|
84
|
+
&& (os.platform() !== 'win32' || Boolean(process.env.WT_SESSION));
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Palette
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// Roles, not colours. A caller asking for `danger` should never care whether
|
|
91
|
+
// that is truecolor crimson or plain ANSI red.
|
|
92
|
+
const DARK_PALETTE = {
|
|
93
|
+
accent: [ 96, 205, 255],
|
|
94
|
+
agent: [138, 180, 248],
|
|
95
|
+
user: [ 90, 200, 160],
|
|
96
|
+
success: [126, 199, 108],
|
|
97
|
+
warn: [230, 190, 90],
|
|
98
|
+
danger: [235, 110, 110],
|
|
99
|
+
muted: [130, 138, 150],
|
|
100
|
+
ink: [225, 228, 234],
|
|
101
|
+
rule: [ 62, 68, 80],
|
|
102
|
+
// The scroll rail. A deliberately DARK blue, not the accent.
|
|
103
|
+
//
|
|
104
|
+
// The rail is orientation, not information you act on — it says where you are
|
|
105
|
+
// while you read something else, so it must be legible without competing with
|
|
106
|
+
// the transcript for attention. In `accent` cyan it was the brightest thing on
|
|
107
|
+
// a screen full of prose. Dark enough to recede, blue enough to read as one
|
|
108
|
+
// continuous object beside the markers, which keep their own roles.
|
|
109
|
+
scroll: [ 58, 110, 170],
|
|
110
|
+
// Background roles. opencode fills its input surface with theme
|
|
111
|
+
// backgroundElement and marks it with a left border in the agent's colour
|
|
112
|
+
// (component/prompt/index.tsx: border={["left"]} + backgroundColor). These are
|
|
113
|
+
// that fill: one step off the terminal's own background, never a block of
|
|
114
|
+
// colour that fights the text sitting on it.
|
|
115
|
+
surface: [ 28, 31, 38],
|
|
116
|
+
surfaceHi:[ 38, 42, 51]
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const LIGHT_PALETTE = {
|
|
120
|
+
accent: [ 0, 110, 170],
|
|
121
|
+
agent: [ 40, 80, 170],
|
|
122
|
+
user: [ 20, 120, 90],
|
|
123
|
+
success: [ 30, 120, 40],
|
|
124
|
+
warn: [155, 110, 0],
|
|
125
|
+
danger: [185, 40, 40],
|
|
126
|
+
muted: [ 95, 102, 112],
|
|
127
|
+
ink: [ 25, 28, 34],
|
|
128
|
+
rule: [200, 204, 212],
|
|
129
|
+
scroll: [ 60, 100, 160],
|
|
130
|
+
surface: [242, 244, 247],
|
|
131
|
+
surfaceHi:[232, 235, 240]
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Fallback for 16-colour terminals: the closest base ANSI code per role.
|
|
135
|
+
const BASIC_CODES = {
|
|
136
|
+
accent: 36, agent: 34, user: 32, success: 32,
|
|
137
|
+
warn: 33, danger: 31, muted: 90, ink: 39, rule: 90,
|
|
138
|
+
scroll: 34,
|
|
139
|
+
surface: 39, surfaceHi: 39
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Background codes for the 16-colour fallback. A base-16 terminal has no shade
|
|
143
|
+
// between "black" and "bright black", so a surface fill there would be a slab of
|
|
144
|
+
// solid grey behind the text — worse than no fill. It gets none, and the left
|
|
145
|
+
// bar alone carries the block. See fill().
|
|
146
|
+
const BASIC_BG_CODES = { surface: 0, surfaceHi: 100 };
|
|
147
|
+
|
|
148
|
+
const PALETTE = IS_LIGHT ? LIGHT_PALETTE : DARK_PALETTE;
|
|
149
|
+
|
|
150
|
+
function rgbTo256(r, g, b) {
|
|
151
|
+
// Greyscale ramp first — it is far more accurate for the muted/rule roles
|
|
152
|
+
// than the 6×6×6 cube, which has coarse steps near grey.
|
|
153
|
+
if (Math.abs(r - g) < 12 && Math.abs(g - b) < 12) {
|
|
154
|
+
const level = Math.round(((r + g + b) / 3 - 8) / 247 * 24);
|
|
155
|
+
return 232 + Math.max(0, Math.min(23, level));
|
|
156
|
+
}
|
|
157
|
+
const scale = (value) => Math.round(value / 255 * 5);
|
|
158
|
+
return 16 + 36 * scale(r) + 6 * scale(g) + scale(b);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Open the escape sequence for a role, or '' when colour is off. */
|
|
162
|
+
function open(role) {
|
|
163
|
+
if (!HAS_COLOR) return '';
|
|
164
|
+
const rgb = PALETTE[role];
|
|
165
|
+
if (!rgb) return '';
|
|
166
|
+
if (COLOR_DEPTH >= 16777216) return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
167
|
+
if (COLOR_DEPTH >= 256) return `\x1b[38;5;${rgbTo256(rgb[0], rgb[1], rgb[2])}m`;
|
|
168
|
+
return `\x1b[${BASIC_CODES[role] || 39}m`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const RESET = '\x1b[0m';
|
|
172
|
+
|
|
173
|
+
/** Paint `text` in a semantic role. A no-op when colour is unavailable. */
|
|
174
|
+
export function paint(role, text) {
|
|
175
|
+
const prefix = open(role);
|
|
176
|
+
return prefix ? `${prefix}${text}${RESET}` : String(text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function bold(text) {
|
|
180
|
+
return HAS_COLOR ? `\x1b[1m${text}${RESET}` : String(text);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function dim(text) {
|
|
184
|
+
return HAS_COLOR ? `\x1b[2m${text}${RESET}` : String(text);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Paint a BACKGROUND in a semantic role.
|
|
189
|
+
*
|
|
190
|
+
* The companion to paint(). opencode gives its input surface and each user
|
|
191
|
+
* message a filled background plus a left border, which is what makes them read
|
|
192
|
+
* as blocks rather than as more lines of shell output. This is that fill.
|
|
193
|
+
*
|
|
194
|
+
* Two deliberate degradations:
|
|
195
|
+
* * no colour at all → returns the text untouched, so NO_COLOR still gets a
|
|
196
|
+
* clean transcript and the left bar alone carries the block;
|
|
197
|
+
* * 16-colour terminals → also untouched, because the only greys available
|
|
198
|
+
* there are black and bright-black, and a slab of either behind body text is
|
|
199
|
+
* less readable than no fill.
|
|
200
|
+
*
|
|
201
|
+
* The caller pads the text to the block width BEFORE calling: a background only
|
|
202
|
+
* reads as a surface if it extends past the last character.
|
|
203
|
+
*/
|
|
204
|
+
export function openFill(role) {
|
|
205
|
+
if (!HAS_COLOR) return '';
|
|
206
|
+
const rgb = PALETTE[role];
|
|
207
|
+
if (!rgb) return '';
|
|
208
|
+
if (COLOR_DEPTH >= 16777216) return `\x1b[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
209
|
+
if (COLOR_DEPTH >= 256) return `\x1b[48;5;${rgbTo256(rgb[0], rgb[1], rgb[2])}m`;
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function fill(role, text) {
|
|
214
|
+
const open = openFill(role);
|
|
215
|
+
if (!open) return String(text);
|
|
216
|
+
|
|
217
|
+
// Re-open the background after every reset inside the text.
|
|
218
|
+
//
|
|
219
|
+
// THIS is why the box did not look like a box. paint() and dim() each close
|
|
220
|
+
// with \x1b[0m, and \x1b[0m resets EVERY attribute — including the background
|
|
221
|
+
// this function just set. So the fill survived only as far as the first
|
|
222
|
+
// styled word: on a 99-column row the model line was filled for 6 columns and
|
|
223
|
+
// bare for the other 93, and the "surface" read as a stray highlight.
|
|
224
|
+
//
|
|
225
|
+
// Re-establishing the background immediately after each reset costs a few
|
|
226
|
+
// bytes per row and makes the fill continuous whatever the caller styled.
|
|
227
|
+
const body = String(text).split(RESET).join(RESET + open);
|
|
228
|
+
return `${open}${body}${RESET}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A filled badge — used for risk banners, where a fill reads faster than text. */
|
|
232
|
+
export function badge(role, text) {
|
|
233
|
+
if (!HAS_COLOR) return `[${text}]`;
|
|
234
|
+
const rgb = PALETTE[role];
|
|
235
|
+
const label = ` ${text} `;
|
|
236
|
+
if (COLOR_DEPTH >= 16777216) {
|
|
237
|
+
return `\x1b[48;2;${rgb[0]};${rgb[1]};${rgb[2]}m\x1b[38;2;16;16;20m\x1b[1m${label}${RESET}`;
|
|
238
|
+
}
|
|
239
|
+
if (COLOR_DEPTH >= 256) {
|
|
240
|
+
return `\x1b[48;5;${rgbTo256(rgb[0], rgb[1], rgb[2])}m\x1b[30m\x1b[1m${label}${RESET}`;
|
|
241
|
+
}
|
|
242
|
+
return `\x1b[${(BASIC_CODES[role] || 39) + 10}m\x1b[30m${label}${RESET}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Glyph set, degrading to ASCII where Unicode is unsafe. */
|
|
246
|
+
export const GLYPHS = HAS_UNICODE
|
|
247
|
+
? {
|
|
248
|
+
arrow: '▸', check: '✓', cross: '✕', dot: '·', bullet: '•',
|
|
249
|
+
spinner: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
|
|
250
|
+
hLine: '─', vLine: '│', corner: '└', tee: '├', ellipsis: '…',
|
|
251
|
+
// The left accent bar of a block. A half-width bar sits INSIDE its column
|
|
252
|
+
// rather than filling it, so it reads as an edge instead of as a wall —
|
|
253
|
+
// the same weight opencode's border={["left"]} carries.
|
|
254
|
+
bar: '▌', step: '▪',
|
|
255
|
+
// The permission marker. opencode shows a warning triangle and NO risk
|
|
256
|
+
// word — the level is internal plumbing, and printing 'DANGEROUS' at
|
|
257
|
+
// someone just makes them numb to it. Colour still carries the tier.
|
|
258
|
+
warnMark: '△',
|
|
259
|
+
// A CONTINUOUS thick rule, for the one place that needs the eye to read a
|
|
260
|
+
// single unbroken edge: the input box. `▌` is a half BLOCK — it does not
|
|
261
|
+
// fill the cell's full height in most terminal fonts, so a column of them
|
|
262
|
+
// comes out as ragged stubs with gaps nobody chose. `┃` is box-drawing
|
|
263
|
+
// heavy vertical, which is drawn to join between rows.
|
|
264
|
+
barHeavy: '┃',
|
|
265
|
+
// Rounded panel corners, for the session header.
|
|
266
|
+
topLeft: '╭', topRight: '╮', bottomLeft: '╰', bottomRight: '╯'
|
|
267
|
+
}
|
|
268
|
+
: {
|
|
269
|
+
arrow: '>', check: 'v', cross: 'x', dot: '.', bullet: '*',
|
|
270
|
+
spinner: ['|', '/', '-', '\\'],
|
|
271
|
+
hLine: '-', vLine: '|', corner: '`', tee: '+', ellipsis: '...',
|
|
272
|
+
bar: '|', step: '*',
|
|
273
|
+
warnMark: '!',
|
|
274
|
+
barHeavy: '|',
|
|
275
|
+
topLeft: '+', topRight: '+', bottomLeft: '+', bottomRight: '+'
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Does the terminal turn OSC 8 sequences into clickable links?
|
|
280
|
+
*
|
|
281
|
+
* Deliberately an allowlist. A terminal that does not understand OSC 8 mostly
|
|
282
|
+
* ignores it, but some older ones print the URL as literal text in the middle
|
|
283
|
+
* of a line — so a wrong guess here is visible corruption, and the cost of
|
|
284
|
+
* guessing "no" is only that a path is not clickable.
|
|
285
|
+
*/
|
|
286
|
+
function detectHyperlinkSupport() {
|
|
287
|
+
if (!HAS_COLOR) return false; // piped, or colour opted out
|
|
288
|
+
if (process.env.CAUSYNE_NO_LINKS === '1') return false;
|
|
289
|
+
if (process.env.TERM_PROGRAM === 'Apple_Terminal') return false; // no OSC 8 support
|
|
290
|
+
|
|
291
|
+
return Boolean(
|
|
292
|
+
process.env.WT_SESSION || // Windows Terminal
|
|
293
|
+
process.env.KITTY_WINDOW_ID ||
|
|
294
|
+
process.env.VTE_VERSION || // GNOME Terminal, Tilix
|
|
295
|
+
process.env.WEZTERM_PANE ||
|
|
296
|
+
['iTerm.app', 'WezTerm', 'ghostty', 'vscode', 'Hyper'].includes(process.env.TERM_PROGRAM)
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export const HAS_HYPERLINKS = detectHyperlinkSupport();
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Wrap `text` as a clickable link to `url`.
|
|
304
|
+
*
|
|
305
|
+
* ONLY for URLs this process builds itself — a workspace file path, a spill
|
|
306
|
+
* file. Never for a URL taken from tool output or from a model reply: OSC 8 is
|
|
307
|
+
* an escape sequence, and emitting one on behalf of untrusted content is the
|
|
308
|
+
* injection that src/tui/text.js sanitize() exists to prevent.
|
|
309
|
+
*
|
|
310
|
+
* For a URL that DID come from a model reply, use safeLink() below, which
|
|
311
|
+
* rebuilds the URL from a parsed object instead of trusting the string.
|
|
312
|
+
*/
|
|
313
|
+
export function link(text, url) {
|
|
314
|
+
if (!HAS_HYPERLINKS) return String(text);
|
|
315
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* A link for a URL that came from a model reply or from tool output.
|
|
320
|
+
*
|
|
321
|
+
* The rule above still holds: OSC 8 is an escape sequence, and passing
|
|
322
|
+
* untrusted text into one is an injection. The way to publish a link anyway is
|
|
323
|
+
* not to trust the string — it is to stop treating it as a string. The URL is
|
|
324
|
+
* parsed, rejected unless it is plain http/https, and then RE-SERIALISED from
|
|
325
|
+
* the parsed object through encodeURI, so what reaches the terminal is a URL
|
|
326
|
+
* this process constructed. A payload carrying \x1b, \x07, an OSC terminator or
|
|
327
|
+
* a `javascript:` scheme cannot survive that round trip, and gets printed as
|
|
328
|
+
* ordinary text instead.
|
|
329
|
+
*
|
|
330
|
+
* Returns the plain text unchanged whenever the URL does not clear the bar.
|
|
331
|
+
*/
|
|
332
|
+
export function safeLink(text, url) {
|
|
333
|
+
if (!HAS_HYPERLINKS) return String(text);
|
|
334
|
+
|
|
335
|
+
const raw = String(url || '');
|
|
336
|
+
// 2048 is the practical URL ceiling; beyond it this is a payload, not a link.
|
|
337
|
+
if (raw.length === 0 || raw.length > 2048) return String(text);
|
|
338
|
+
// Any control byte disqualifies the string before it is even parsed.
|
|
339
|
+
// eslint-disable-next-line no-control-regex
|
|
340
|
+
if (/[\x00-\x1f\x7f]/.test(raw)) return String(text);
|
|
341
|
+
|
|
342
|
+
let parsed;
|
|
343
|
+
try {
|
|
344
|
+
parsed = new URL(raw);
|
|
345
|
+
} catch (e) {
|
|
346
|
+
return String(text);
|
|
347
|
+
}
|
|
348
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return String(text);
|
|
349
|
+
if (!parsed.hostname) return String(text);
|
|
350
|
+
|
|
351
|
+
// Built from the PARSED url, not from the input.
|
|
352
|
+
const safeUrl = encodeURI(parsed.href).replace(/[\x00-\x1f\x7f]/g, '');
|
|
353
|
+
return `\x1b]8;;${safeUrl}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** A file path as a clickable file:// link, absolute as OSC 8 requires. */
|
|
357
|
+
export function fileLink(text, absolutePath) {
|
|
358
|
+
if (!HAS_HYPERLINKS || !absolutePath) return String(text);
|
|
359
|
+
return link(text, 'file://' + encodeURI(absolutePath));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* The terminal's ACTUAL width. Chrome fills it.
|
|
364
|
+
*
|
|
365
|
+
* This used to clamp to 120. Stretching a window to 220 columns therefore left
|
|
366
|
+
* 101 of them empty: the input box, its fill and the footer all stopped dead
|
|
367
|
+
* two thirds of the way across while the terminal carried on. A hardcoded
|
|
368
|
+
* ceiling cannot follow a window the user is resizing, and the whole point of
|
|
369
|
+
* the box is that it is the surface they are typing into.
|
|
370
|
+
*/
|
|
371
|
+
export function terminalWidth() {
|
|
372
|
+
const raw = process.stdout.columns || 80;
|
|
373
|
+
return Math.max(40, raw);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The widest a line of PROSE may be, given the space available.
|
|
378
|
+
*
|
|
379
|
+
* Chrome and text want opposite things from a wide terminal. A box should fill
|
|
380
|
+
* it; a paragraph should not — 200 characters per line is genuinely hard to
|
|
381
|
+
* read, because the eye loses the return sweep. Typographic practice puts the
|
|
382
|
+
* comfortable range at 45-75 characters and the outer limit near 100, which is
|
|
383
|
+
* exactly what native-skills/design/type-scale-and-hierarchy tells the agent to
|
|
384
|
+
* do for anyone else's interface.
|
|
385
|
+
*
|
|
386
|
+
* Expressed as a share of the width rather than a fixed number, so it still
|
|
387
|
+
* responds to the terminal instead of pinning at one value.
|
|
388
|
+
*/
|
|
389
|
+
export const READABLE_MAX = 100;
|
|
390
|
+
|
|
391
|
+
export function readableWidth(available = terminalWidth()) {
|
|
392
|
+
return Math.max(40, Math.min(available, READABLE_MAX));
|
|
393
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Curated tool display — what a tool call looks like to a human.
|
|
2
|
+
//
|
|
3
|
+
// The renderer used to print raw argument JSON, so a file write showed as
|
|
4
|
+
//
|
|
5
|
+
// fs_write {"path":"/Users/example/Projects/site/index.html","content":"<!DOC…
|
|
6
|
+
//
|
|
7
|
+
// which buries the one thing that matters (WHICH file) behind the one thing
|
|
8
|
+
// that does not (the entire payload). Every tool here declares a verb and the
|
|
9
|
+
// single detail worth reading; paths are shortened to `~`, credential-shaped
|
|
10
|
+
// values are masked, and anything unmapped falls back to a generic summary so a
|
|
11
|
+
// newly added tool can never render broken.
|
|
12
|
+
//
|
|
13
|
+
// Pure JS over src/tui/text.js. No dependency, no shell.
|
|
14
|
+
|
|
15
|
+
import { shortenHome, safeDisplay } from './text.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pull the first present field, as a string.
|
|
19
|
+
* Tools disagree about what to call their path argument — that inconsistency is
|
|
20
|
+
* documented in sandbox.js PATH_ARGUMENTS and is the reason this exists.
|
|
21
|
+
*/
|
|
22
|
+
function firstString(args, keys) {
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const value = args[key];
|
|
25
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function basename(pathish) {
|
|
31
|
+
const cleaned = String(pathish).replace(/[/\\]+$/, '');
|
|
32
|
+
const parts = cleaned.split(/[/\\]/);
|
|
33
|
+
return parts[parts.length - 1] || cleaned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Path detail: the filename, with just enough parent to disambiguate. */
|
|
37
|
+
function pathDetail(value, { full = false } = {}) {
|
|
38
|
+
if (!value) return '';
|
|
39
|
+
const shortened = shortenHome(value);
|
|
40
|
+
if (full) return shortened;
|
|
41
|
+
|
|
42
|
+
const parts = shortened.split(/[/\\]/).filter(Boolean);
|
|
43
|
+
if (parts.length <= 2) return shortened;
|
|
44
|
+
return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hostOf(url) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url);
|
|
50
|
+
return parsed.hostname + (parsed.pathname !== '/' ? parsed.pathname : '');
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return String(url);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
const TOOL_DISPLAY = new Map([
|
|
58
|
+
['fs_read', (a) => ({ verb: 'read', detail: pathDetail(firstString(a, ['path', 'file_path'])) })],
|
|
59
|
+
['fs_write', (a) => ({ verb: 'write', detail: pathDetail(firstString(a, ['path', 'file_path'])) })],
|
|
60
|
+
['fs_edit', (a) => ({ verb: 'edit', detail: editDetail(a, 'edits') })],
|
|
61
|
+
['MultiEdit', (a) => ({ verb: 'edit', detail: editDetail(a, 'edits') })],
|
|
62
|
+
['fs_delete', (a) => ({ verb: 'delete', detail: pathDetail(firstString(a, ['path']), { full: true }) })],
|
|
63
|
+
['fs_move', (a) => ({ verb: 'move', detail: `${pathDetail(a.source)} → ${pathDetail(a.destination)}` })],
|
|
64
|
+
['fs_list', (a) => ({ verb: 'list', detail: pathDetail(firstString(a, ['path', 'directory']), { full: true }) })],
|
|
65
|
+
['fs_find', (a) => ({ verb: 'find', detail: firstString(a, ['pattern', 'query', 'name']) })],
|
|
66
|
+
['fs_search', (a) => ({ verb: 'search', detail: firstString(a, ['query', 'pattern']) })],
|
|
67
|
+
|
|
68
|
+
['sysRun', (a) => ({ verb: 'run', detail: a.command })],
|
|
69
|
+
['sysRunBg', (a) => ({ verb: 'run bg', detail: a.command })],
|
|
70
|
+
['terminal_persistent_shell', (a) => ({ verb: 'shell', detail: firstString(a, ['command', 'input']) })],
|
|
71
|
+
['proc_term', (a) => ({ verb: 'kill', detail: firstString(a, ['pid', 'name']) })],
|
|
72
|
+
['process_list', () => ({ verb: 'list', detail: 'processes' })],
|
|
73
|
+
|
|
74
|
+
['gitOp', (a) => ({ verb: 'git', detail: firstString(a, ['command', 'action', 'operation']) })],
|
|
75
|
+
['web_fetch', (a) => ({ verb: 'fetch', detail: hostOf(a.url) })],
|
|
76
|
+
['web_search', (a) => ({ verb: 'search', detail: firstString(a, ['query', 'q']) })],
|
|
77
|
+
['browser_act', (a) => ({ verb: 'browse', detail: firstString(a, ['url', 'action']) })],
|
|
78
|
+
['screenshot', (a) => ({ verb: 'capture', detail: firstString(a, ['target', 'url']) || 'screen' })],
|
|
79
|
+
|
|
80
|
+
['code_search_ripgrep', (a) => ({ verb: 'grep', detail: firstString(a, ['query', 'pattern']) })],
|
|
81
|
+
['codebase_search', (a) => ({ verb: 'search', detail: firstString(a, ['query']) })],
|
|
82
|
+
['code_verify_syntax', (a) => ({ verb: 'verify', detail: pathDetail(firstString(a, ['path', 'file_path'])) })],
|
|
83
|
+
|
|
84
|
+
['memory_write', (a) => ({ verb: 'remember', detail: firstString(a, ['fact', 'key', 'project']) })],
|
|
85
|
+
['TodoWrite', (a) => ({ verb: 'plan', detail: countDetail(a.todos, 'item') })],
|
|
86
|
+
['update_plan', (a) => ({ verb: 'plan', detail: countDetail(a.plan, 'step') })],
|
|
87
|
+
['tool_search', (a) => ({ verb: 'discover', detail: firstString(a, ['query', 'description']) })],
|
|
88
|
+
['fs_read_skill', (a) => ({ verb: 'skill', detail: firstString(a, ['id', 'skillId', 'domain']) })],
|
|
89
|
+
|
|
90
|
+
['manage_local_dependencies', (a) => ({ verb: 'provision', detail: `${a.action || ''} ${a.group || a.package || ''}`.trim() })]
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
function editDetail(args, listKey) {
|
|
94
|
+
const path = pathDetail(firstString(args, ['path', 'file_path']));
|
|
95
|
+
const edits = Array.isArray(args[listKey]) ? args[listKey].length : 0;
|
|
96
|
+
return edits > 1 ? `${path} (${edits} edits)` : path;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function countDetail(list, noun) {
|
|
100
|
+
const count = Array.isArray(list) ? list.length : 0;
|
|
101
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generic fallback for any tool without a curated entry.
|
|
106
|
+
*
|
|
107
|
+
* Ordered so the field that actually carries meaning shows first — the same
|
|
108
|
+
* ordering the approval prompt uses, for the same reason.
|
|
109
|
+
*/
|
|
110
|
+
const FALLBACK_KEYS = [
|
|
111
|
+
'command', 'path', 'file_path', 'source', 'url', 'query', 'action',
|
|
112
|
+
'name', 'pattern', 'directory', 'target'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
function fallbackDisplay(toolName, args) {
|
|
116
|
+
const detail = firstString(args, FALLBACK_KEYS);
|
|
117
|
+
if (detail) return { verb: '', detail: shortenHome(detail) };
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const compact = JSON.stringify(args);
|
|
121
|
+
if (compact && compact !== '{}') return { verb: '', detail: compact };
|
|
122
|
+
} catch (e) { /* circular or unserialisable: nothing useful to show */ }
|
|
123
|
+
return { verb: '', detail: '' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a tool call to `{ verb, detail }` fit for one terminal row.
|
|
128
|
+
*
|
|
129
|
+
* Never throws: a display helper must not be able to break the transcript.
|
|
130
|
+
*/
|
|
131
|
+
export function describeTool(toolName, args) {
|
|
132
|
+
const safeArgs = args && typeof args === 'object' ? args : {};
|
|
133
|
+
let display;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const curated = TOOL_DISPLAY.get(toolName);
|
|
137
|
+
display = curated ? curated(safeArgs) : fallbackDisplay(toolName, safeArgs);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
display = fallbackDisplay(toolName, safeArgs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
verb: safeDisplay(display.verb || ''),
|
|
144
|
+
// Untrusted: an argument can carry escapes or a credential the model echoed.
|
|
145
|
+
detail: safeDisplay(display.detail || '').replace(/\s+/g, ' ').trim()
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** True when a curated entry exists — used by tests and by /help style output. */
|
|
150
|
+
export function hasCuratedDisplay(toolName) {
|
|
151
|
+
return TOOL_DISPLAY.has(toolName);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Multi-line detail for an approval prompt, where the user needs more than one
|
|
156
|
+
* row. Reuses the same ordering and the same redaction.
|
|
157
|
+
*/
|
|
158
|
+
export function describeToolForApproval(args) {
|
|
159
|
+
const safeArgs = args && typeof args === 'object' ? args : {};
|
|
160
|
+
const lines = [];
|
|
161
|
+
|
|
162
|
+
for (const key of FALLBACK_KEYS) {
|
|
163
|
+
const value = safeArgs[key];
|
|
164
|
+
if (typeof value !== 'string' || value.length === 0) continue;
|
|
165
|
+
lines.push(`${key}: ${safeDisplay(shortenHome(value))}`);
|
|
166
|
+
if (lines.length >= 4) break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (lines.length === 0) {
|
|
170
|
+
try {
|
|
171
|
+
const compact = JSON.stringify(safeArgs);
|
|
172
|
+
if (compact && compact !== '{}') lines.push(safeDisplay(compact));
|
|
173
|
+
} catch (e) { /* nothing showable */ }
|
|
174
|
+
}
|
|
175
|
+
return lines;
|
|
176
|
+
}
|