@causyne/agent 0.0.0-stage → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/ask.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// One question, asked properly, outside the chat surface.
|
|
2
|
+
//
|
|
3
|
+
// `causyne config` and `causyne pair` run with no chat session behind them, so
|
|
4
|
+
// there is no live region for a dialog to draw into. That is why they were the
|
|
5
|
+
// last two surfaces still on inquirer — and inquirer is a different design
|
|
6
|
+
// system: its own colours, its own glyphs, no NO_COLOR handling, and a list
|
|
7
|
+
// widget that looks nothing like the picker the same user sees inside `causyne`.
|
|
8
|
+
//
|
|
9
|
+
// The fix is NOT a second app shell. That was tried once: screen.js, layout.js,
|
|
10
|
+
// transcript.js and appShell.js existed, took over the terminal, and were
|
|
11
|
+
// deleted under UX-01 when it turned out there is no way to detect a terminal
|
|
12
|
+
// that ignores the alternate buffer. Rebuilding a host would walk straight back
|
|
13
|
+
// into that.
|
|
14
|
+
//
|
|
15
|
+
// Instead this borrows the host that already works. ChatPrompt owns raw mode,
|
|
16
|
+
// key decoding, the bounded live region and the layer stack, and `attach()` —
|
|
17
|
+
// the part that wires it to a chat renderer — is optional. So a standalone
|
|
18
|
+
// question is just a ChatPrompt started for the lifetime of one dialog and
|
|
19
|
+
// stopped again: same keys, same panel, same width clamping, same normal-buffer
|
|
20
|
+
// discipline, no second implementation to drift.
|
|
21
|
+
//
|
|
22
|
+
// Every helper degrades. If stdin is not a terminal, or raw mode cannot be
|
|
23
|
+
// taken, they fall back to a plain readline question — a better editor must
|
|
24
|
+
// never be the reason a command refuses to run.
|
|
25
|
+
|
|
26
|
+
import readline from 'readline';
|
|
27
|
+
import { ChatPrompt, isInteractive } from './prompt.js';
|
|
28
|
+
import { SelectLayer, InputLayer, ConfirmLayer } from './dialogs.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Run one layer on a temporary prompt host.
|
|
32
|
+
*
|
|
33
|
+
* `build(resolve)` returns the Layer to show and calls `resolve` with the
|
|
34
|
+
* answer. The host is torn down before this settles, so whatever the caller
|
|
35
|
+
* prints next lands in a clean terminal.
|
|
36
|
+
*/
|
|
37
|
+
function hostOneLayer(build) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let prompt;
|
|
40
|
+
let settled = false;
|
|
41
|
+
|
|
42
|
+
const finish = (value) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
settled = true;
|
|
45
|
+
try { if (prompt) prompt.stop(); } catch (e) { /* already stopped */ }
|
|
46
|
+
// Let the terminal settle before the caller writes over the same rows.
|
|
47
|
+
setImmediate(() => resolve(value));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
prompt = new ChatPrompt({
|
|
52
|
+
commands: [],
|
|
53
|
+
// ctrl+c during a question is a cancel, not an exit: the caller decides
|
|
54
|
+
// what "no answer" means for it. An exit here would kill the process
|
|
55
|
+
// mid-configuration and leave a half-written config.json.
|
|
56
|
+
onInterrupt: () => finish(null),
|
|
57
|
+
onExit: () => finish(null)
|
|
58
|
+
});
|
|
59
|
+
prompt.start();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
resolve(undefined); // signals "no TTY host" — caller falls back
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
prompt.openLayer(build(finish));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** True when a panel-based question can be asked at all. */
|
|
70
|
+
export function canAskInPanel() {
|
|
71
|
+
return isInteractive();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Plain readline, for pipes, CI and any terminal that refuses raw mode. */
|
|
75
|
+
function fallbackQuestion(question, { mask = false } = {}) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
rl.question(`${question} `, (answer) => { rl.close(); resolve(answer); });
|
|
79
|
+
if (mask) {
|
|
80
|
+
// Suppress the echo of what is typed. `_writeToOutput` is readline's own
|
|
81
|
+
// hook for exactly this; the prompt itself still has to be drawn or the
|
|
82
|
+
// user sees nothing at all.
|
|
83
|
+
rl._writeToOutput = function (text) {
|
|
84
|
+
if (text.includes(question)) rl.output.write(text);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pick one of a list.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} options
|
|
94
|
+
* @param {string} options.title
|
|
95
|
+
* @param {Array} options.items `{ id, label, description }`
|
|
96
|
+
* @returns {Promise<object|null>} the chosen item, or null if dismissed
|
|
97
|
+
*/
|
|
98
|
+
export async function askSelect({ title = '', items = [] } = {}) {
|
|
99
|
+
if (items.length === 0) return null;
|
|
100
|
+
|
|
101
|
+
if (canAskInPanel()) {
|
|
102
|
+
const answer = await hostOneLayer((resolve) => new SelectLayer({
|
|
103
|
+
title,
|
|
104
|
+
items: items.map((item) => ({ ...item, value: item })),
|
|
105
|
+
onPick: (picked) => resolve(picked ? picked.value : null)
|
|
106
|
+
}));
|
|
107
|
+
if (answer !== undefined) return answer;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Numbered list is the honest fallback: it needs no cursor control at all.
|
|
111
|
+
console.log(`\n${title}`);
|
|
112
|
+
items.forEach((item, index) => {
|
|
113
|
+
console.log(` ${String(index + 1).padStart(2)}. ${item.label}${item.description ? ` — ${item.description}` : ''}`);
|
|
114
|
+
});
|
|
115
|
+
const raw = await fallbackQuestion(`Choose 1-${items.length}:`);
|
|
116
|
+
const choice = Number(String(raw).trim());
|
|
117
|
+
return Number.isInteger(choice) && choice >= 1 && choice <= items.length ? items[choice - 1] : null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ask for a line of text.
|
|
122
|
+
*
|
|
123
|
+
* @returns {Promise<string|null>} the answer, or null if dismissed
|
|
124
|
+
*/
|
|
125
|
+
export async function askText({ title = '', lines = [], value = '', placeholder = '', mask = false, validate = null } = {}) {
|
|
126
|
+
while (true) {
|
|
127
|
+
let answer;
|
|
128
|
+
|
|
129
|
+
if (canAskInPanel()) {
|
|
130
|
+
answer = await hostOneLayer((resolve) => new InputLayer({
|
|
131
|
+
title, lines, value, placeholder, mask,
|
|
132
|
+
onSubmit: (text) => resolve(text),
|
|
133
|
+
onCancel: () => resolve(null)
|
|
134
|
+
}));
|
|
135
|
+
if (answer === undefined) answer = await fallbackQuestion(`${title}:`, { mask });
|
|
136
|
+
} else {
|
|
137
|
+
if (lines.length) console.log('\n' + lines.join('\n'));
|
|
138
|
+
answer = await fallbackQuestion(`${title}:`, { mask });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (answer === null) return null;
|
|
142
|
+
|
|
143
|
+
// Re-ask rather than accept something the caller has said is wrong. This is
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
const problem = validate ? validate(answer) : null;
|
|
147
|
+
if (!problem) return answer;
|
|
148
|
+
console.log(` ${problem}`);
|
|
149
|
+
value = answer;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A yes/no, defaulting to no on dismissal. */
|
|
154
|
+
export async function askConfirm(message) {
|
|
155
|
+
if (canAskInPanel()) {
|
|
156
|
+
const answer = await hostOneLayer((resolve) => new ConfirmLayer({
|
|
157
|
+
message,
|
|
158
|
+
onAnswer: (yes) => resolve(Boolean(yes))
|
|
159
|
+
}));
|
|
160
|
+
if (answer !== undefined) return Boolean(answer);
|
|
161
|
+
}
|
|
162
|
+
const raw = await fallbackQuestion(`${message} [y/N]`);
|
|
163
|
+
return /^y(es)?$/i.test(String(raw).trim());
|
|
164
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// The banner: the wordmark, and the first thing a new user is invited to ask.
|
|
2
|
+
//
|
|
3
|
+
// This lives in its own module because it is written ONCE, straight into the
|
|
4
|
+
// user's real terminal history, before anything else runs — the same shape
|
|
5
|
+
// openclaw uses (src/cli/banner.ts emitCliBanner: art first, metadata line
|
|
6
|
+
// underneath) and opencode's entry splash, whose own comment calls these
|
|
7
|
+
// "scrollback snapshots ... immutable terminal history once committed".
|
|
8
|
+
//
|
|
9
|
+
// Nothing here repaints. Nothing here clears. See UX-01 in .agents/rules.json.
|
|
10
|
+
//
|
|
11
|
+
// Pure Node over theme.js and text.js. No dependency, no shell.
|
|
12
|
+
|
|
13
|
+
import { paint, bold, dim, GLYPHS, HAS_UNICODE } from './theme.js';
|
|
14
|
+
import { visibleWidth, truncate, padEnd, shortenHome } from './text.js';
|
|
15
|
+
|
|
16
|
+
// Painted through the theme rather than with a hardcoded \x1b[1m\x1b[36m, so it
|
|
17
|
+
// still obeys NO_COLOR and still degrades on a terminal without box-drawing.
|
|
18
|
+
const WORDMARK = [
|
|
19
|
+
' ████ ███ █ █ ████ █ █ █ █ █████',
|
|
20
|
+
'█ █ █ █ █ █ █ █ ██ █ █ ',
|
|
21
|
+
'█ █████ █ █ ███ █ █ █ █ ████ ',
|
|
22
|
+
'█ █ █ █ █ █ █ █ ██ █ ',
|
|
23
|
+
' ████ █ █ ███ ████ █ █ █ █████'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// A terminal without box-drawing support would render the art as mojibake, so
|
|
27
|
+
// it gets the wordmark spelled out instead of a field of question marks.
|
|
28
|
+
const WORDMARK_PLAIN = ['C A U S Y N E'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The banner rows.
|
|
32
|
+
*
|
|
33
|
+
* `unicode` is overridable so both branches can actually be tested — under
|
|
34
|
+
* `node --test` there is no TTY, so HAS_UNICODE is false and the block art would
|
|
35
|
+
* never be exercised at all.
|
|
36
|
+
*/
|
|
37
|
+
export function wordmark(width = 80, { unicode = HAS_UNICODE } = {}) {
|
|
38
|
+
const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
|
|
39
|
+
const rows = art.map((line) => paint('accent', line));
|
|
40
|
+
rows.push('');
|
|
41
|
+
rows.push(dim('Causyne AI Agent'));
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The session header: the wordmark and the session facts, SIDE BY SIDE.
|
|
47
|
+
*
|
|
48
|
+
* This used to be three stacked blocks — six rows of art, then a blank, then a
|
|
49
|
+
* bordered panel of four label/value rows, then two separate hint lines. Nineteen
|
|
50
|
+
* rows of chrome before the user could type a character, on a surface whose whole
|
|
51
|
+
* job is to get out of the way.
|
|
52
|
+
*
|
|
53
|
+
* Neither reference does that. claude-code sets its art in the left column with
|
|
54
|
+
* the version, model, plan and directory stacked beside it — three rows, one
|
|
55
|
+
* block. opencode puts the logo alone and the facts in the prompt box. The
|
|
56
|
+
* common property is that the identity and the facts are ONE object, not a
|
|
57
|
+
* sequence of them.
|
|
58
|
+
*
|
|
59
|
+
* Falls back to stacking when the terminal cannot hold both columns, because a
|
|
60
|
+
* header that wraps is worse than a header that is tall.
|
|
61
|
+
*/
|
|
62
|
+
export function sessionBanner(info = {}, width = 80, { unicode = HAS_UNICODE } = {}) {
|
|
63
|
+
const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
|
|
64
|
+
const artWidth = Math.max(...art.map(visibleWidth));
|
|
65
|
+
|
|
66
|
+
const facts = [];
|
|
67
|
+
if (info.version) facts.push([bold(paint('accent', 'Causyne')), dim(info.version)]);
|
|
68
|
+
const model = info.model || 'no model configured';
|
|
69
|
+
facts.push([paint('ink', model), info.provider ? dim(providerLabel(info.provider)) : '']);
|
|
70
|
+
const mode = [info.autonomyMode ? `mode ${info.autonomyMode}` : '', info.tier ? `tier ${info.tier}` : '']
|
|
71
|
+
.filter(Boolean).join(` ${GLYPHS.dot} `);
|
|
72
|
+
if (mode) facts.push([dim(mode), '']);
|
|
73
|
+
if (info.workspace) facts.push([dim(shortenHome(info.workspace)), '']);
|
|
74
|
+
|
|
75
|
+
const GAP = 4;
|
|
76
|
+
// The spare column is the same one the stacked branch below already reserves
|
|
77
|
+
// (`width - 1`). Without it a full-width fact row lands in the terminal's
|
|
78
|
+
// deferred-wrap state: the cursor sits past the last column, and the next
|
|
79
|
+
// write decides whether that becomes a wrap. Measured on an 80-column pty,
|
|
80
|
+
// the workspace row came back at exactly 80 while every other row was 79.
|
|
81
|
+
const room = width - artWidth - GAP - 1;
|
|
82
|
+
|
|
83
|
+
// Not enough width for two columns: stack, rather than wrap either one.
|
|
84
|
+
if (room < 24) {
|
|
85
|
+
return art.map((line) => paint('accent', line))
|
|
86
|
+
.concat([''], facts.map(([left, right]) =>
|
|
87
|
+
truncate(right ? `${left} ${right}` : left, width - 1, GLYPHS.ellipsis)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rows = [];
|
|
91
|
+
const height = Math.max(art.length, facts.length);
|
|
92
|
+
for (let index = 0; index < height; index++) {
|
|
93
|
+
const artLine = art[index] === undefined ? '' : paint('accent', art[index]);
|
|
94
|
+
const fact = facts[index];
|
|
95
|
+
const left = padEnd(artLine, artWidth) + ' '.repeat(GAP);
|
|
96
|
+
if (!fact) { rows.push(artLine); continue; }
|
|
97
|
+
const [primary, secondary] = fact;
|
|
98
|
+
const text = secondary ? `${primary} ${secondary}` : primary;
|
|
99
|
+
rows.push(left + truncate(text, room, GLYPHS.ellipsis));
|
|
100
|
+
}
|
|
101
|
+
return rows;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Display the provider the way the user wrote it, normalised for casing only.
|
|
106
|
+
*
|
|
107
|
+
* Causyne is BYOK: the provider is whatever the user configured, and nothing here
|
|
108
|
+
* may assume or default to one. A config saying `Gemini` and one saying `gemini`
|
|
109
|
+
* are the same provider and must read the same.
|
|
110
|
+
*/
|
|
111
|
+
export function providerLabel(provider) {
|
|
112
|
+
const raw = String(provider || '').trim();
|
|
113
|
+
if (!raw) return '';
|
|
114
|
+
const KNOWN = {
|
|
115
|
+
openai: 'OpenAI', anthropic: 'Anthropic', gemini: 'Gemini', ollama: 'Ollama',
|
|
116
|
+
deepseek: 'DeepSeek', dashscope: 'DashScope', 'dashscope-byok': 'DashScope',
|
|
117
|
+
qwen: 'Qwen', xai: 'xAI', grok: 'xAI', moonshot: 'Moonshot', kimi: 'Moonshot',
|
|
118
|
+
muse: 'Meta', meta: 'Meta',
|
|
119
|
+
'openai-compatible': 'OpenAI-compatible'
|
|
120
|
+
};
|
|
121
|
+
return KNOWN[raw.toLowerCase()] || raw;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// What to ask, for the person who has just opened this for the first time.
|
|
125
|
+
//
|
|
126
|
+
// `ask anything` is an invitation, not an example — it tells a new user nothing
|
|
127
|
+
// about what this agent can actually be asked to do. Both references show a
|
|
128
|
+
// concrete prompt instead (opencode seeds a random one per launch from
|
|
129
|
+
// routes/home.tsx). One is picked per session so the hint does not churn while
|
|
130
|
+
// the user is reading it.
|
|
131
|
+
export const EXAMPLES = [
|
|
132
|
+
'summarise what changed in this repo today',
|
|
133
|
+
'find every TODO left in src/ and group them by file',
|
|
134
|
+
'explain what this project does and how it is wired',
|
|
135
|
+
'run the tests and tell me what broke',
|
|
136
|
+
'review my last commit for anything risky'
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
/** One example, chosen per session rather than per keystroke. */
|
|
140
|
+
export function pickExample() {
|
|
141
|
+
return EXAMPLES[Math.floor(Math.random() * EXAMPLES.length)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Join hint fragments, dropping the least important until the line fits.
|
|
146
|
+
*
|
|
147
|
+
* Same policy as the footer: a hint clipped mid-word reads as a bug, so drop
|
|
148
|
+
* whole fragments rather than truncate one, and fall back entirely when even a
|
|
149
|
+
* single fragment will not fit.
|
|
150
|
+
*/
|
|
151
|
+
export function fitHints(parts, width, fallback) {
|
|
152
|
+
const separator = ` ${GLYPHS.dot} `;
|
|
153
|
+
const kept = parts.slice();
|
|
154
|
+
while (kept.length > 1 && visibleWidth(kept.join(separator)) > width) kept.pop();
|
|
155
|
+
const line = kept.join(separator);
|
|
156
|
+
// A narrow terminal must not get `try "review my last commit for anything r`.
|
|
157
|
+
return visibleWidth(line) <= width ? line : fallback;
|
|
158
|
+
}
|
package/src/tui/block.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Blocks: a left accent bar and a filled surface, the shape that makes a run of
|
|
2
|
+
// terminal rows read as ONE thing.
|
|
3
|
+
//
|
|
4
|
+
// Causyne's chat printed every kind of row the same way — the user's message, the
|
|
5
|
+
// agent's reply, an aside from the loop and the prompt itself were all just
|
|
6
|
+
// lines in the scrollback, distinguishable only by a leading glyph. On a busy
|
|
7
|
+
// turn there was no visual seam anywhere, which is the difference the reference
|
|
8
|
+
// tools actually make. opencode gives each of those a left border in the agent's
|
|
9
|
+
// colour and a background one step off the terminal's own
|
|
10
|
+
// (component/prompt/index.tsx:1353 — `border={["left"]}` + `borderColor` +
|
|
11
|
+
// `backgroundColor={theme.backgroundElement}` + `paddingLeft={2}`).
|
|
12
|
+
//
|
|
13
|
+
// Two properties make it work, and both are easy to get wrong:
|
|
14
|
+
//
|
|
15
|
+
// * EVERY ROW IS PADDED TO THE SAME WIDTH. A background only reads as a surface
|
|
16
|
+
// if it runs past the last character; ragged rows read as highlighted text.
|
|
17
|
+
// * THE PADDING IS MEASURED, NOT COUNTED. padEnd() on a coloured string pads by
|
|
18
|
+
// byte length, so the escape sequences count toward the column and the right
|
|
19
|
+
// edge of the block frays. text.js padEnd() measures visible width.
|
|
20
|
+
//
|
|
21
|
+
// It degrades in two steps rather than one: without truecolor/256 there is no
|
|
22
|
+
// fill and the bar alone carries the block; without colour at all the bar is an
|
|
23
|
+
// ASCII `|`. A transcript piped to a file stays clean either way.
|
|
24
|
+
//
|
|
25
|
+
// Pure Node over ./theme.js and ./text.js. No dependency, no shell.
|
|
26
|
+
|
|
27
|
+
import { paint, fill, dim, GLYPHS, HAS_COLOR } from './theme.js';
|
|
28
|
+
import { visibleWidth, padEnd, truncate, wrap } from './text.js';
|
|
29
|
+
|
|
30
|
+
// Columns between the bar and the text. opencode uses paddingLeft={2} inside its
|
|
31
|
+
// bordered box; one column belongs to the bar, so the gutter is one more.
|
|
32
|
+
const GUTTER = 1;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render `lines` as one block with a left accent bar.
|
|
36
|
+
*
|
|
37
|
+
* @param {string[]} lines already-styled rows; NOT re-wrapped (see blockOf)
|
|
38
|
+
* @param {object} options
|
|
39
|
+
* @param {number} options.width total columns the block may occupy
|
|
40
|
+
* @param {string} options.role accent role for the bar (user, agent, accent…)
|
|
41
|
+
* @param {boolean} options.filled paint the surface behind the text
|
|
42
|
+
* @param {string} options.surface which surface role to fill with
|
|
43
|
+
* @returns {string[]} rows, each exactly `width` columns wide
|
|
44
|
+
*/
|
|
45
|
+
export function barBlock(lines, {
|
|
46
|
+
width = 80,
|
|
47
|
+
role = 'accent',
|
|
48
|
+
filled = true,
|
|
49
|
+
surface = 'surface',
|
|
50
|
+
punctuate = false
|
|
51
|
+
} = {}) {
|
|
52
|
+
// The input box asks for a continuous heavy rule with a dot at its middle —
|
|
53
|
+
// long line, dot, long line. Everything else keeps the half-block bar, because
|
|
54
|
+
// a message block is content and the input box is chrome you type INTO; the
|
|
55
|
+
// reference draws that distinction too, giving its prompt box custom border
|
|
56
|
+
// characters no message uses (component/prompt/index.tsx:1354-1358).
|
|
57
|
+
//
|
|
58
|
+
// Punctuation needs three rows to read as "middle": below that it would land
|
|
59
|
+
// on the only row there is, and a dot alone is not a rule.
|
|
60
|
+
const glyph = punctuate ? GLYPHS.barHeavy : GLYPHS.bar;
|
|
61
|
+
const middle = punctuate && lines.length >= 3 ? Math.floor(lines.length / 2) : -1;
|
|
62
|
+
|
|
63
|
+
const barWidth = visibleWidth(glyph);
|
|
64
|
+
const body = Math.max(1, width - barWidth - GUTTER);
|
|
65
|
+
const bar = paint(role, glyph);
|
|
66
|
+
const marked = paint(role, GLYPHS.bullet);
|
|
67
|
+
const gutter = ' '.repeat(GUTTER);
|
|
68
|
+
|
|
69
|
+
return lines.map((line, index) => {
|
|
70
|
+
// Clip first, then pad: a line longer than the block would otherwise push
|
|
71
|
+
// the fill past the right edge and wrap, which is the failure ChatPrompt
|
|
72
|
+
// clampRows() exists to prevent one layer up.
|
|
73
|
+
const clipped = visibleWidth(line) > body ? truncate(line, body, GLYPHS.ellipsis) : line;
|
|
74
|
+
const padded = padEnd(clipped, body);
|
|
75
|
+
const inner = filled ? fill(surface, gutter + padded) : gutter + padded;
|
|
76
|
+
return (index === middle ? marked : bar) + inner;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wrap `text` to the block's inner width, then render it as a block.
|
|
82
|
+
*
|
|
83
|
+
* The convenience form for plain prose. Callers that have already styled their
|
|
84
|
+
* rows individually (the prompt editor, a status row) use barBlock directly.
|
|
85
|
+
*/
|
|
86
|
+
export function blockOf(text, options = {}) {
|
|
87
|
+
const width = options.width || 80;
|
|
88
|
+
const inner = Math.max(1, width - visibleWidth(GLYPHS.bar) - GUTTER);
|
|
89
|
+
const lines = wrap(String(text == null ? '' : text), inner);
|
|
90
|
+
return barBlock(lines.length ? lines : [''], options);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A bordered panel with its title inset into the top border.
|
|
95
|
+
*
|
|
96
|
+
* The session header used to be two horizontal rules with label/value rows
|
|
97
|
+
* between them, which reads as a table of settings rather than as a greeting.
|
|
98
|
+
* Both references open with a bordered panel instead — claude-code insets the
|
|
99
|
+
* product name and version into the top rule exactly this way.
|
|
100
|
+
*
|
|
101
|
+
* `rows` are already-styled strings. Returns rows of exactly `width` columns.
|
|
102
|
+
*/
|
|
103
|
+
export function panel(rows, { width = 80, title = '', role = 'rule' } = {}) {
|
|
104
|
+
const inner = Math.max(4, width - 2);
|
|
105
|
+
const h = GLYPHS.hLine;
|
|
106
|
+
const label = title ? ` ${title} ` : '';
|
|
107
|
+
const clippedLabel = visibleWidth(label) > inner - 2 ? '' : label;
|
|
108
|
+
const lead = clippedLabel ? h : '';
|
|
109
|
+
const trail = h.repeat(Math.max(0, inner - visibleWidth(clippedLabel) - visibleWidth(lead)));
|
|
110
|
+
|
|
111
|
+
const top = paint(role, GLYPHS.topLeft + lead)
|
|
112
|
+
+ (clippedLabel ? paint('accent', clippedLabel) : '')
|
|
113
|
+
+ paint(role, trail + GLYPHS.topRight);
|
|
114
|
+
const bottom = paint(role, GLYPHS.bottomLeft + h.repeat(inner) + GLYPHS.bottomRight);
|
|
115
|
+
|
|
116
|
+
const side = paint(role, GLYPHS.vLine);
|
|
117
|
+
const middle = rows.map((row) => {
|
|
118
|
+
const clipped = visibleWidth(row) > inner ? truncate(row, inner, GLYPHS.ellipsis) : row;
|
|
119
|
+
return side + padEnd(clipped, inner) + side;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return [top, ...middle, bottom];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `9.2K (5%)` — what the context window has left, at a glance.
|
|
127
|
+
*
|
|
128
|
+
* Returns '' rather than a misleading 0% when no limit is known: an invented
|
|
129
|
+
* denominator is worse than a missing number.
|
|
130
|
+
*
|
|
131
|
+
* BOTH ARGUMENTS MUST BE IN THE SAME UNIT. The caller was passing a cumulative
|
|
132
|
+
* TOKEN count and a CHARACTER pool, which is how a quarter-full window came to
|
|
133
|
+
* report 99%.
|
|
134
|
+
*/
|
|
135
|
+
export function contextMeter(usedTokens, limitTokens) {
|
|
136
|
+
const used = Number(usedTokens) || 0;
|
|
137
|
+
const limit = Number(limitTokens) || 0;
|
|
138
|
+
if (used <= 0) return '';
|
|
139
|
+
const compact = used < 1000
|
|
140
|
+
? String(Math.round(used))
|
|
141
|
+
: used < 1000000
|
|
142
|
+
? `${(used / 1000).toFixed(used < 10000 ? 1 : 0)}K`
|
|
143
|
+
: `${(used / 1000000).toFixed(1)}M`;
|
|
144
|
+
if (limit <= 0) return compact;
|
|
145
|
+
// NOT clamped to 99. The clamp was there to keep the meter from looking like
|
|
146
|
+
// a hard stop, and its real effect was to hide a 242% reading behind a
|
|
147
|
+
// plausible-looking one for a whole session. A meter that cannot report
|
|
148
|
+
// "over" is a meter that cannot report the only thing worth acting on.
|
|
149
|
+
return `${compact} (${Math.round((used / limit) * 100)}%)`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export { GUTTER };
|