carthagent 1.0.7 → 1.1.0-beta.2
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/README.md +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/beta-rewards-e2e-plan.md +48 -0
- package/docs/beta-rewards-e2e-results.md +84 -0
- package/docs/cloud-onboarding-acceptance.md +65 -0
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/launch-readiness-live-results.md +65 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +175 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-beta.1.md +60 -0
- package/docs/release-notes-beta.2.md +60 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/cloud/client.mjs +13 -7
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +19 -6
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1188 -0
- package/lib/opencode/cli.mjs +253 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +22 -5
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/providers/names.mjs +1 -1
- package/lib/tui/framing.mjs +1 -80
- package/lib/tui/models.mjs +1 -1
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +243 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +65 -36
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +71 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +280 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +506 -0
- package/plugins/carthagent/src/ui/client.ts +127 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +165 -0
- package/plugins/carthagent/src/ui/voyages.tsx +131 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +64 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/cloud-client.test.mjs +2 -2
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +57 -2
- package/tests/delivery.test.mjs +22 -1
- package/tests/experiential-extension.test.mjs +2 -2
- package/tests/experiential.test.mjs +1 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +55 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +525 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +33 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-guidance.test.mjs +16 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +83 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +397 -0
- package/tests/native-voyages-ui.test.mjs +307 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +248 -20
- package/tests/opencode-runtime.test.mjs +597 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +231 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/tui.test.mjs +2 -2
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +45 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +882 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +91 -65
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,3670 @@
|
|
|
1
|
+
diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx
|
|
2
|
+
index 88939ebc852824e6ef2371c719a528ab75092a7c..6c16e376aa10f28e767e4fba17c2220158330b53 100644
|
|
3
|
+
--- a/packages/tui/src/app.tsx
|
|
4
|
+
+++ b/packages/tui/src/app.tsx
|
|
5
|
+
@@ -2,6 +2,7 @@ import { existsSync } from "fs"
|
|
6
|
+
import path from "path"
|
|
7
|
+
import { render, useRenderer, useTerminalDimensions } from "@opentui/solid"
|
|
8
|
+
import { registerOpencodeSpinner } from "./component/register-spinner"
|
|
9
|
+
+import { chromeEllipsis, chromeLabel } from "./ui/glyphs"
|
|
10
|
+
import { Effect, Latch } from "effect"
|
|
11
|
+
import { Service, type Endpoint } from "@opencode/client/effect/service"
|
|
12
|
+
import { OpenCode, type SessionInfo } from "@opencode/client"
|
|
13
|
+
@@ -618,7 +619,7 @@ function App() {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
- renderer.setTerminalTitle(`OC | ${title.length > 40 ? title.slice(0, 37) + "…" : title}`)
|
|
18
|
+
+ renderer.setTerminalTitle(`OC | ${title.length > 40 ? title.slice(0, 37) + chromeEllipsis() : title}`)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@@ -1046,7 +1047,7 @@ function App() {
|
|
23
|
+
const restart = client.restart
|
|
24
|
+
if (!restart) return
|
|
25
|
+
dialog.clear()
|
|
26
|
+
- toast.show({ variant: "info", message: "Restarting service…", duration: 30000 })
|
|
27
|
+
+ toast.show({ variant: "info", message: chromeLabel("Restarting service…"), duration: 30000 })
|
|
28
|
+
// restart resolves once the replacement service is healthy; the
|
|
29
|
+
// event stream reattaches through the reconnect loop.
|
|
30
|
+
await restart()
|
|
31
|
+
@@ -1063,7 +1064,7 @@ function App() {
|
|
32
|
+
slash: { name: "reload" },
|
|
33
|
+
run: async () => {
|
|
34
|
+
dialog.clear()
|
|
35
|
+
- toast.show({ variant: "info", message: "Reloading configuration…", duration: 30000 })
|
|
36
|
+
+ toast.show({ variant: "info", message: chromeLabel("Reloading configuration…"), duration: 30000 })
|
|
37
|
+
await client.api.location
|
|
38
|
+
.reload()
|
|
39
|
+
.then(() => {
|
|
40
|
+
diff --git a/packages/tui/src/component/command-palette.tsx b/packages/tui/src/component/command-palette.tsx
|
|
41
|
+
index 4f46db3253589b4cc05bc32cb1c12087f2663cab..51390b70b51378d4de227a285870d909c4faf2b7 100644
|
|
42
|
+
--- a/packages/tui/src/component/command-palette.tsx
|
|
43
|
+
+++ b/packages/tui/src/component/command-palette.tsx
|
|
44
|
+
@@ -3,6 +3,7 @@ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
|
|
45
|
+
import { type DialogContext } from "../ui/dialog"
|
|
46
|
+
import { COMMAND_PALETTE_COMMAND, Keymap, type KeymapCommand } from "../context/keymap"
|
|
47
|
+
import { DialogConfig, settingID, settings } from "./dialog-config"
|
|
48
|
+
+import { chromeStatusSeparator } from "../ui/glyphs"
|
|
49
|
+
|
|
50
|
+
function isSuggestedPaletteCommand(command: KeymapCommand) {
|
|
51
|
+
const suggested = command.suggested
|
|
52
|
+
@@ -23,7 +24,7 @@ export function CommandPaletteDialog() {
|
|
53
|
+
description: command.description,
|
|
54
|
+
category: command.group,
|
|
55
|
+
searchText: [command.id, command.description].filter(Boolean).join(" "),
|
|
56
|
+
- searchFooter: [command.group, footer].filter(Boolean).join(" · "),
|
|
57
|
+
+ searchFooter: [command.group, footer].filter(Boolean).join(` ${chromeStatusSeparator()} `),
|
|
58
|
+
footer,
|
|
59
|
+
value: command.id,
|
|
60
|
+
suggested: isSuggestedPaletteCommand(command),
|
|
61
|
+
@@ -38,7 +39,7 @@ export function CommandPaletteDialog() {
|
|
62
|
+
title: setting.title,
|
|
63
|
+
category: setting.category,
|
|
64
|
+
searchText: setting.keywords?.join(" "),
|
|
65
|
+
- searchFooter: `Settings · ${setting.category}`,
|
|
66
|
+
+ searchFooter: `Settings ${chromeStatusSeparator()} ${setting.category}`,
|
|
67
|
+
value: `setting:${settingID(setting)}`,
|
|
68
|
+
onSelect: (dialog: DialogContext) => {
|
|
69
|
+
dialog.replace(() => <DialogConfig current={settingID(setting)} />)
|
|
70
|
+
diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx
|
|
71
|
+
index 10af2019d600530eb9275a05216fe658e255c066..259b561364c8b9fd5c71afe9d647991ed9ff6817 100644
|
|
72
|
+
--- a/packages/tui/src/component/devtools-bar.tsx
|
|
73
|
+
+++ b/packages/tui/src/component/devtools-bar.tsx
|
|
74
|
+
@@ -17,6 +17,7 @@ import { useDialog } from "../ui/dialog"
|
|
75
|
+
import { DialogExperiments } from "./dialog-experiments"
|
|
76
|
+
import { usePlugin } from "../plugin/context"
|
|
77
|
+
import { errorMessage } from "../util/error"
|
|
78
|
+
+import { chromeGlyph, chromeLabel } from "../ui/glyphs"
|
|
79
|
+
|
|
80
|
+
const graphWidth = 23
|
|
81
|
+
const sampleIntervalMilliseconds = 2_000
|
|
82
|
+
@@ -254,7 +255,7 @@ export function DevToolsBar() {
|
|
83
|
+
: theme.text.base
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
- {serverIndicator().icon}
|
|
87
|
+
+ {chromeGlyph(serverIndicator().icon)}
|
|
88
|
+
</text>
|
|
89
|
+
<text
|
|
90
|
+
fg={
|
|
91
|
+
@@ -360,7 +361,7 @@ export function DevToolsBar() {
|
|
92
|
+
<PanelBox>
|
|
93
|
+
<PanelTitle>Tools</PanelTitle>
|
|
94
|
+
<Action onClick={() => void dump()} disabled={dumping()} hoverBackground>
|
|
95
|
+
- {dumping() ? "Writing debug snapshot…" : "Write debug snapshot"}
|
|
96
|
+
+ {dumping() ? chromeLabel("Writing debug snapshot…") : "Write debug snapshot"}
|
|
97
|
+
</Action>
|
|
98
|
+
<Show when={dumpPath()}>
|
|
99
|
+
{(file) => (
|
|
100
|
+
@@ -579,15 +580,15 @@ export function runtimeStatus(samples: readonly Readonly<{ delay: number; time:
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function statusIcon(status: RuntimeStatus) {
|
|
104
|
+
- if (status === "high") return "●"
|
|
105
|
+
- if (status === "medium") return "⦿"
|
|
106
|
+
- return "○"
|
|
107
|
+
+ if (status === "high") return chromeGlyph("●")
|
|
108
|
+
+ if (status === "medium") return chromeGlyph("⦿")
|
|
109
|
+
+ return chromeGlyph("○")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function connectionIndicator(status: "connected" | "connecting" | "reconnecting", attempt: number) {
|
|
113
|
+
- if (status === "connected") return { state: "connected" as const, icon: "✓" }
|
|
114
|
+
- if (status === "reconnecting" && attempt >= 3) return { state: "disconnected" as const, icon: "×" }
|
|
115
|
+
- return { state: "reconnecting" as const, icon: "↻" }
|
|
116
|
+
+ if (status === "connected") return { state: "connected" as const, icon: chromeGlyph("✓") }
|
|
117
|
+
+ if (status === "reconnecting" && attempt >= 3) return { state: "disconnected" as const, icon: chromeGlyph("×") }
|
|
118
|
+
+ return { state: "reconnecting" as const, icon: chromeGlyph("↻") }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function brailleGraph(values: readonly number[], width = graphWidth) {
|
|
122
|
+
diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx
|
|
123
|
+
index f2e3f28b837d0af38f5ada1dc7b4fe3856462bcd..2e0d04234e730cb10c5a468647d456ea364391db 100644
|
|
124
|
+
--- a/packages/tui/src/component/dialog-config.tsx
|
|
125
|
+
+++ b/packages/tui/src/component/dialog-config.tsx
|
|
126
|
+
@@ -2,6 +2,7 @@ import { createMemo, createSignal } from "solid-js"
|
|
127
|
+
import { useConfig, useNewDesigns } from "../config"
|
|
128
|
+
import { useThemes } from "../context/theme"
|
|
129
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
130
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
131
|
+
import { useToast } from "../ui/toast"
|
|
132
|
+
|
|
133
|
+
type Setting = {
|
|
134
|
+
@@ -402,7 +403,7 @@ export function DialogConfig(props: { current?: string }) {
|
|
135
|
+
filterThreshold={0.7}
|
|
136
|
+
onMove={(option) => setSelected(option.value)}
|
|
137
|
+
onSelect={(option) => void change(1, option.value)}
|
|
138
|
+
- footerHints={newDesigns() ? [] : [{ title: "change", label: "←/→" }]}
|
|
139
|
+
+ footerHints={newDesigns() ? [] : [{ title: "change", label: `${chromeGlyph("←")}/${chromeGlyph("→")}` }]}
|
|
140
|
+
bindings={[
|
|
141
|
+
{
|
|
142
|
+
bind: "left",
|
|
143
|
+
diff --git a/packages/tui/src/component/dialog-debug.tsx b/packages/tui/src/component/dialog-debug.tsx
|
|
144
|
+
index 546f47c56c64eef5d5f2954d36dfb7ce4231e43d..1c1d4b81ab37461dc56bbb8c32571a0af928fadb 100644
|
|
145
|
+
--- a/packages/tui/src/component/dialog-debug.tsx
|
|
146
|
+
+++ b/packages/tui/src/component/dialog-debug.tsx
|
|
147
|
+
@@ -9,6 +9,7 @@ import { useToast } from "../ui/toast"
|
|
148
|
+
import { describeOS, describeTerminal } from "../util/system"
|
|
149
|
+
import { useTuiApp } from "../context/runtime"
|
|
150
|
+
import { useNewDesigns } from "../config"
|
|
151
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
152
|
+
|
|
153
|
+
export function DialogDebug() {
|
|
154
|
+
const theme = useTheme()
|
|
155
|
+
@@ -54,7 +55,7 @@ export function DialogDebug() {
|
|
156
|
+
}))
|
|
157
|
+
|
|
158
|
+
const copyAction = () => ({
|
|
159
|
+
- title: copied() ? "✓ copied" : "copy",
|
|
160
|
+
+ title: copied() ? `${chromeGlyph("✓")} copied` : "copy",
|
|
161
|
+
key: newDesigns() ? "c" : "enter",
|
|
162
|
+
color: copied() ? theme.text.feedback.success.base : undefined,
|
|
163
|
+
onTrigger: copy,
|
|
164
|
+
diff --git a/packages/tui/src/component/dialog-error-details.tsx b/packages/tui/src/component/dialog-error-details.tsx
|
|
165
|
+
index fe8f122903e22b2b1cc20c07cdcf52fef044c783..ff8d521c992dbc219dc1ecf6f120e42936179c50 100644
|
|
166
|
+
--- a/packages/tui/src/component/dialog-error-details.tsx
|
|
167
|
+
+++ b/packages/tui/src/component/dialog-error-details.tsx
|
|
168
|
+
@@ -11,6 +11,7 @@ import { useTheme } from "../context/theme"
|
|
169
|
+
import { emptyPrompt } from "../prompt/history"
|
|
170
|
+
import { DIALOG_INSET, DialogFooter, DialogHeader, DialogLayout, dialogWidth, useDialog } from "../ui/dialog"
|
|
171
|
+
import { FilePath } from "../ui/file-path"
|
|
172
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
173
|
+
import { useToast } from "../ui/toast"
|
|
174
|
+
import { errorDetails } from "../util/error-details"
|
|
175
|
+
|
|
176
|
+
@@ -64,7 +65,7 @@ export function DialogErrorDetails(props: {
|
|
177
|
+
const actions = () => [
|
|
178
|
+
{ title: "investigate", key: "i", onTrigger: investigate },
|
|
179
|
+
copied()
|
|
180
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: copy }
|
|
181
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: copy }
|
|
182
|
+
: { title: "copy details", key: "c", onTrigger: copy },
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
@@ -109,7 +110,9 @@ export function DialogErrorDetails(props: {
|
|
186
|
+
</box>
|
|
187
|
+
}
|
|
188
|
+
footer={{ actions: { primary: actions() } }}
|
|
189
|
+
- legacyFooter={<DialogFooter actions={{ secondary: actions(), primary: [{ title: "scroll", key: "↑/↓" }] }} />}
|
|
190
|
+
+ legacyFooter={
|
|
191
|
+
+ <DialogFooter actions={{ secondary: actions(), primary: [{ title: "scroll", key: `${chromeGlyph("↑")}/${chromeGlyph("↓")}` }] }} />
|
|
192
|
+
+ }
|
|
193
|
+
>
|
|
194
|
+
<scrollbox
|
|
195
|
+
ref={(element: ScrollBoxRenderable) => (scroll = element)}
|
|
196
|
+
diff --git a/packages/tui/src/component/dialog-experiments.tsx b/packages/tui/src/component/dialog-experiments.tsx
|
|
197
|
+
index a8830000f88043ddaaafb100cf080932536cebcc..db00317858710f7e8ae08225b858e1d825591c43 100644
|
|
198
|
+
--- a/packages/tui/src/component/dialog-experiments.tsx
|
|
199
|
+
+++ b/packages/tui/src/component/dialog-experiments.tsx
|
|
200
|
+
@@ -3,6 +3,7 @@ import { useConfig, useNewDesigns } from "../config"
|
|
201
|
+
import { DIALOG_LIST_INSET } from "../ui/dialog"
|
|
202
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
203
|
+
import { useTheme } from "../context/theme"
|
|
204
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
205
|
+
import { useToast } from "../ui/toast"
|
|
206
|
+
|
|
207
|
+
type Experiment = {
|
|
208
|
+
@@ -68,7 +69,11 @@ export function DialogExperiments() {
|
|
209
|
+
<text fg={theme.text.muted}>No experiments available</text>
|
|
210
|
+
</box>
|
|
211
|
+
}
|
|
212
|
+
- footerHints={experiments.length > 0 && !newDesigns() ? [{ title: "change", label: "←/→" }] : []}
|
|
213
|
+
+ footerHints={
|
|
214
|
+
+ experiments.length > 0 && !newDesigns()
|
|
215
|
+
+ ? [{ title: "change", label: `${chromeGlyph("←")}/${chromeGlyph("→")}` }]
|
|
216
|
+
+ : []
|
|
217
|
+
+ }
|
|
218
|
+
bindings={
|
|
219
|
+
experiments.length > 0
|
|
220
|
+
? [
|
|
221
|
+
diff --git a/packages/tui/src/component/dialog-highlights.tsx b/packages/tui/src/component/dialog-highlights.tsx
|
|
222
|
+
index d11506eae7ababa0b7b7f8cbda571cc330296252..b04827d3b4ed44733931c2ea3d1ae1ef8f1dbd3e 100644
|
|
223
|
+
--- a/packages/tui/src/component/dialog-highlights.tsx
|
|
224
|
+
+++ b/packages/tui/src/component/dialog-highlights.tsx
|
|
225
|
+
@@ -6,6 +6,7 @@ import { Keymap } from "../context/keymap"
|
|
226
|
+
import type { TuiHighlight } from "../context/runtime"
|
|
227
|
+
import { useTheme } from "../context/theme"
|
|
228
|
+
import { DIALOG_INSET, DialogBleed, DialogFooter, DialogHeader, DialogLayout, useDialog } from "../ui/dialog"
|
|
229
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
230
|
+
import { getScrollAcceleration } from "../util/scroll"
|
|
231
|
+
|
|
232
|
+
export function DialogHighlights(props: { version: string; items: ReadonlyArray<TuiHighlight> }) {
|
|
233
|
+
@@ -38,7 +39,7 @@ export function DialogHighlights(props: { version: string; items: ReadonlyArray<
|
|
234
|
+
footer={{ actions: { primary: [{ title: "ok", key: "enter", onTrigger: () => dialog.clear() }] } }}
|
|
235
|
+
legacyFooter={
|
|
236
|
+
<box flexDirection="row" justifyContent="space-between" alignItems="center">
|
|
237
|
+
- <DialogFooter actions={{ secondary: [{ title: "scroll", key: "↑/↓" }] }} />
|
|
238
|
+
+ <DialogFooter actions={{ secondary: [{ title: "scroll", key: `${chromeGlyph("↑")}/${chromeGlyph("↓")}` }] }} />
|
|
239
|
+
<box
|
|
240
|
+
paddingLeft={3}
|
|
241
|
+
paddingRight={3}
|
|
242
|
+
diff --git a/packages/tui/src/component/dialog-image-preview.tsx b/packages/tui/src/component/dialog-image-preview.tsx
|
|
243
|
+
index f2bdd1d9e1b642dcd8707d585735f45fa6996a76..ae7811480ed9c02dbc9de58c1b38bc536d36ad23 100644
|
|
244
|
+
--- a/packages/tui/src/component/dialog-image-preview.tsx
|
|
245
|
+
+++ b/packages/tui/src/component/dialog-image-preview.tsx
|
|
246
|
+
@@ -2,6 +2,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|
247
|
+
import { createMemo, createSignal } from "solid-js"
|
|
248
|
+
import { Keymap } from "../context/keymap"
|
|
249
|
+
import { useTheme } from "../context/theme"
|
|
250
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
251
|
+
import { DialogHeader, DialogLayout, useDialog } from "../ui/dialog"
|
|
252
|
+
|
|
253
|
+
type ImagePreviewItem = Readonly<{
|
|
254
|
+
@@ -44,8 +45,8 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
|
|
255
|
+
primary:
|
|
256
|
+
props.images.length > 1
|
|
257
|
+
? [
|
|
258
|
+
- { title: "previous", key: "←", onTrigger: () => move(-1) },
|
|
259
|
+
- { title: "next", key: "→", onTrigger: () => move(1) },
|
|
260
|
+
+ { title: "previous", key: chromeGlyph("←"), onTrigger: () => move(-1) },
|
|
261
|
+
+ { title: "next", key: chromeGlyph("→"), onTrigger: () => move(1) },
|
|
262
|
+
]
|
|
263
|
+
: [],
|
|
264
|
+
},
|
|
265
|
+
@@ -60,13 +61,13 @@ export function DialogImagePreview(props: { images: readonly ImagePreviewItem[];
|
|
266
|
+
legacyFooter={
|
|
267
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
268
|
+
<text fg={theme.text.muted} onMouseUp={() => move(-1)}>
|
|
269
|
+
- {props.images.length > 1 ? "← previous" : ""}
|
|
270
|
+
+ {props.images.length > 1 ? `${chromeGlyph("←")} previous` : ""}
|
|
271
|
+
</text>
|
|
272
|
+
<text fg={failed() ? theme.text.feedback.error.base : theme.text.muted} wrapMode="none" truncate>
|
|
273
|
+
{failed() ? "No preview" : (current().mention?.text ?? `Image ${index() + 1}`)}
|
|
274
|
+
</text>
|
|
275
|
+
<text fg={theme.text.muted} onMouseUp={() => move(1)}>
|
|
276
|
+
- {props.images.length > 1 ? "next →" : ""}
|
|
277
|
+
+ {props.images.length > 1 ? `next ${chromeGlyph("→")}` : ""}
|
|
278
|
+
</text>
|
|
279
|
+
</box>
|
|
280
|
+
}
|
|
281
|
+
diff --git a/packages/tui/src/component/dialog-integration.tsx b/packages/tui/src/component/dialog-integration.tsx
|
|
282
|
+
index 183e8df3af9f5449fd4e7dfc66f157071df31fb4..b346ae61839431fa60dc3e82dcf62aeb28778ac9 100644
|
|
283
|
+
--- a/packages/tui/src/component/dialog-integration.tsx
|
|
284
|
+
+++ b/packages/tui/src/component/dialog-integration.tsx
|
|
285
|
+
@@ -33,6 +33,7 @@ import {
|
|
286
|
+
} from "../ui/dialog"
|
|
287
|
+
import { DialogPrompt } from "../ui/dialog-prompt"
|
|
288
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
289
|
+
+import { chromeGlyph, chromeLabel } from "../ui/glyphs"
|
|
290
|
+
import { Link } from "../ui/link"
|
|
291
|
+
import { useToast } from "../ui/toast"
|
|
292
|
+
import { errorMessage } from "../util/error"
|
|
293
|
+
@@ -135,13 +136,13 @@ export function DialogIntegration(
|
|
294
|
+
title: integration.name,
|
|
295
|
+
value: integration.id,
|
|
296
|
+
description: methods.length === 0 ? "Environment only" : undefined,
|
|
297
|
+
- footer: status ? "Sign in required →" : connectionSummary(integration) || undefined,
|
|
298
|
+
+ footer: status ? `Sign in required ${chromeGlyph("→")}` : connectionSummary(integration) || undefined,
|
|
299
|
+
footerColor: status ? theme.text.feedback.warning.base : undefined,
|
|
300
|
+
category,
|
|
301
|
+
disabled: methods.length === 0 && credentials.length === 0,
|
|
302
|
+
gutter:
|
|
303
|
+
integration.connections.length > 0 && !status
|
|
304
|
+
- ? () => <text fg={theme.text.feedback.success.base}>✓</text>
|
|
305
|
+
+ ? () => <text fg={theme.text.feedback.success.base}>{chromeGlyph("✓")}</text>
|
|
306
|
+
: undefined,
|
|
307
|
+
onSelect: () => {
|
|
308
|
+
if (credentials.length) return manageConnections(integration, methods, location, dialog, props.onConnected)
|
|
309
|
+
@@ -218,7 +219,7 @@ function manageConnections(
|
|
310
|
+
? `Press ${shortcuts.get("dialog.integration.delete")} again to confirm`
|
|
311
|
+
: connection.label,
|
|
312
|
+
value: connection.id,
|
|
313
|
+
- footer: connection.status ? "Sign in required →" : undefined,
|
|
314
|
+
+ footer: connection.status ? `Sign in required ${chromeGlyph("→")}` : undefined,
|
|
315
|
+
footerColor: connection.status ? theme.text.feedback.warning.base : undefined,
|
|
316
|
+
category: "Connected accounts",
|
|
317
|
+
bg: confirming ? theme.background.action.destructive.focused : undefined,
|
|
318
|
+
@@ -381,7 +382,7 @@ function ExternalStarting(props: {
|
|
319
|
+
dialog.clear()
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
- return <OAuthView title={props.method.label} message="Connecting…" />
|
|
323
|
+
+ return <OAuthView title={props.method.label} message={chromeLabel("Connecting…")} />
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function beginKey(
|
|
327
|
+
@@ -455,7 +456,7 @@ function CommandStarting(props: {
|
|
328
|
+
if (!handedOff) closed = true
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
- return <CommandView title={props.method.label} output="" message="Starting command…" />
|
|
332
|
+
+ return <CommandView title={props.method.label} output="" message={chromeLabel("Starting command…")} />
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function CommandPending(props: {
|
|
336
|
+
@@ -516,7 +517,7 @@ function CommandPending(props: {
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
- return <CommandView title={props.title} output={output()} message="Waiting for command to finish…" />
|
|
341
|
+
+ return <CommandView title={props.title} output={output()} message={chromeLabel("Waiting for command to finish…")} />
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function CommandView(props: { title: string; output: string; message: string }) {
|
|
345
|
+
@@ -649,7 +650,7 @@ function OAuthStarting(props: {
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
- return <OAuthView title={props.method.label} message="Starting authorization…" />
|
|
350
|
+
+ return <OAuthView title={props.method.label} message={chromeLabel("Starting authorization…")} />
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function OAuthAuto(props: {
|
|
354
|
+
@@ -737,7 +738,7 @@ function OAuthAuto(props: {
|
|
355
|
+
title={props.title}
|
|
356
|
+
url={props.attempt.url}
|
|
357
|
+
instructions={props.attempt.instructions}
|
|
358
|
+
- message="Waiting for authorization…"
|
|
359
|
+
+ message={chromeLabel("Waiting for authorization…")}
|
|
360
|
+
onCopy={copy}
|
|
361
|
+
onOpen={open}
|
|
362
|
+
/>
|
|
363
|
+
@@ -1013,7 +1014,7 @@ function StringChoiceField(props: {
|
|
364
|
+
{ title: "back", key: "esc", onTrigger: back },
|
|
365
|
+
]
|
|
366
|
+
: [
|
|
367
|
+
- { title: "select", key: "↑/↓" },
|
|
368
|
+
+ { title: "select", key: `${chromeGlyph("↑")}/${chromeGlyph("↓")}` },
|
|
369
|
+
{ title: "confirm", key: "enter", onTrigger: () => select(store.selected) },
|
|
370
|
+
{ title: "cancel", key: "esc", onTrigger: () => dialog.clear() },
|
|
371
|
+
],
|
|
372
|
+
@@ -1232,7 +1233,7 @@ async function externalAnswer(
|
|
373
|
+
if (choice === true) return true
|
|
374
|
+
const result = await new Promise<boolean | typeof CANCELLED>((resolve) => {
|
|
375
|
+
dialog.replace(
|
|
376
|
+
- () => <OAuthView title={formLabel(field) || title} message="Opening link…" />,
|
|
377
|
+
+ () => <OAuthView title={formLabel(field) || title} message={chromeLabel("Opening link…")} />,
|
|
378
|
+
() => resolve(CANCELLED),
|
|
379
|
+
)
|
|
380
|
+
void openUrl(field.url).then(
|
|
381
|
+
diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx
|
|
382
|
+
index 68e3778b42ffebbdf0138a05016a58c06cb20dda..aa0582b778e0fb93ad32bcaad13f3357302ed73c 100644
|
|
383
|
+
--- a/packages/tui/src/component/dialog-mcp.tsx
|
|
384
|
+
+++ b/packages/tui/src/component/dialog-mcp.tsx
|
|
385
|
+
@@ -5,6 +5,7 @@ import { pipe, sortBy } from "remeda"
|
|
386
|
+
import { DialogSelect } from "../ui/dialog-select"
|
|
387
|
+
import { useDialog } from "../ui/dialog"
|
|
388
|
+
import { useTheme } from "../context/theme"
|
|
389
|
+
+import { chromeGlyph, chromeLabel } from "../ui/glyphs"
|
|
390
|
+
import { TextAttributes } from "@opentui/core"
|
|
391
|
+
import type { McpServer } from "@opencode/client"
|
|
392
|
+
import { useToast } from "../ui/toast"
|
|
393
|
+
@@ -19,18 +20,18 @@ function statusError(status: McpServer["status"]) {
|
|
394
|
+
|
|
395
|
+
function Status(props: { status: McpServer["status"]; loading: boolean }) {
|
|
396
|
+
if (props.loading || props.status.status === "pending") {
|
|
397
|
+
- return <>Connecting …</>
|
|
398
|
+
+ return <>{chromeLabel("Connecting …")}</>
|
|
399
|
+
}
|
|
400
|
+
if (props.status.status === "connected") {
|
|
401
|
+
- return <span style={{ attributes: TextAttributes.BOLD }}>Connected ✓</span>
|
|
402
|
+
+ return <span style={{ attributes: TextAttributes.BOLD }}>Connected {chromeGlyph("✓")}</span>
|
|
403
|
+
}
|
|
404
|
+
if (props.status.status === "failed") {
|
|
405
|
+
- return <>Failed !</>
|
|
406
|
+
+ return <>Failed {chromeGlyph("✗")}</>
|
|
407
|
+
}
|
|
408
|
+
if (props.status.status === "needs_auth") {
|
|
409
|
+
- return <>Sign in required →</>
|
|
410
|
+
+ return <>Sign in required {chromeGlyph("→")}</>
|
|
411
|
+
}
|
|
412
|
+
- return <>Disabled ○</>
|
|
413
|
+
+ return <>Disabled {chromeGlyph("○")}</>
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function DialogMcp(props: { initialServer?: string; details?: boolean } = {}) {
|
|
417
|
+
diff --git a/packages/tui/src/component/dialog-open.tsx b/packages/tui/src/component/dialog-open.tsx
|
|
418
|
+
index f6d6412c1883ad5a7e61c6904fb17e653c0ea86c..ddab0c6ff55cb6dd1d68cd3015aaea9d0ce8d3ee 100644
|
|
419
|
+
--- a/packages/tui/src/component/dialog-open.tsx
|
|
420
|
+
+++ b/packages/tui/src/component/dialog-open.tsx
|
|
421
|
+
@@ -23,6 +23,7 @@ import { withTimestampedFallback } from "@opencode/util/session-title-fallback"
|
|
422
|
+
import { sessionStatusGutter } from "./dialog-session-list"
|
|
423
|
+
import { Spinner } from "./spinner"
|
|
424
|
+
import { projectName } from "../util/project"
|
|
425
|
+
+import { chromeEllipsis, chromeGlyph, chromeLabel, chromeStatusSeparator } from "../ui/glyphs"
|
|
426
|
+
|
|
427
|
+
const RECENT_LIMIT = 8
|
|
428
|
+
export const DialogOpenKey = Symbol("DialogOpen")
|
|
429
|
+
@@ -211,7 +212,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
430
|
+
const basename = path.basename(session.location.directory)
|
|
431
|
+
const label =
|
|
432
|
+
name && session.location.directory !== project?.canonical && name.toLowerCase() !== basename.toLowerCase()
|
|
433
|
+
- ? `${name} · ${basename}`
|
|
434
|
+
+ ? `${name} ${chromeGlyph("·")} ${basename}`
|
|
435
|
+
: name || basename
|
|
436
|
+
const state = attention(session.id)
|
|
437
|
+
const running =
|
|
438
|
+
@@ -223,13 +224,13 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
439
|
+
searchText: `${session.id} ${session.location.directory}`,
|
|
440
|
+
value: { type: "session", sessionID: session.id } as OpenTarget,
|
|
441
|
+
category: "Sessions",
|
|
442
|
+
- footer: `${label ? `${Locale.truncate(label, 30)} · ` : ""}${timeAgo(session.time.updated)}`,
|
|
443
|
+
+ footer: `${label ? `${Locale.truncate(label, 30, chromeEllipsis())} ${chromeGlyph("·")} ` : ""}${timeAgo(session.time.updated)}`,
|
|
444
|
+
onSelect: () => location.set(session.location),
|
|
445
|
+
gutter: sessionStatusGutter(
|
|
446
|
+
theme,
|
|
447
|
+
state,
|
|
448
|
+
running,
|
|
449
|
+
- tabs.has(session.id) ? () => <text fg={theme.hue.accent[200]}>▪</text> : undefined,
|
|
450
|
+
+ tabs.has(session.id) ? () => <text fg={theme.hue.accent[200]}>{chromeGlyph("▪")}</text> : undefined,
|
|
451
|
+
),
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
@@ -254,7 +255,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
455
|
+
(git ? 2 : 0)
|
|
456
|
+
return {
|
|
457
|
+
title,
|
|
458
|
+
- footer: `${truncateFilePath(footer, width)}${git ? " →" : ""}`,
|
|
459
|
+
+ footer: `${truncateFilePath(footer, width)}${git ? ` ${chromeGlyph("→")}` : ""}`,
|
|
460
|
+
searchText: `${footer} ${projectName(item.project) ?? ""}`,
|
|
461
|
+
value: {
|
|
462
|
+
type: "project",
|
|
463
|
+
@@ -265,7 +266,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
464
|
+
gutter:
|
|
465
|
+
item.directory === current.directory ||
|
|
466
|
+
item.directory === location.current?.project.canonical
|
|
467
|
+
- ? () => <text fg={theme.text.formfield.selected}>●</text>
|
|
468
|
+
+ ? () => <text fg={theme.text.formfield.selected}>{chromeGlyph("●")}</text>
|
|
469
|
+
: undefined,
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
@@ -316,7 +317,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
473
|
+
footer: footer + " ".repeat(Math.max(0, width - stringWidth(footer))),
|
|
474
|
+
value: { type: "project", directory } as OpenTarget,
|
|
475
|
+
gutter:
|
|
476
|
+
- directory === current.directory ? () => <text fg={theme.text.formfield.selected}>●</text> : undefined,
|
|
477
|
+
+ directory === current.directory ? () => <text fg={theme.text.formfield.selected}>{chromeGlyph("●")}</text> : undefined,
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
@@ -336,7 +337,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
482
|
+
restore(previous)
|
|
483
|
+
}}
|
|
484
|
+
title={projectID() ? `${projectName(data.project.get(projectID()!)) ?? "Project"} / Worktrees` : "Open"}
|
|
485
|
+
- placeholder={projectID() ? "Search worktrees…" : "Search sessions and projects…"}
|
|
486
|
+
+ placeholder={projectID() ? chromeLabel("Search worktrees…") : chromeLabel("Search sessions and projects…")}
|
|
487
|
+
options={projectID() ? worktreeOptions() : options()}
|
|
488
|
+
current={
|
|
489
|
+
currentSessionID() ? ({ type: "session", sessionID: currentSessionID()! } as OpenTarget) : undefined
|
|
490
|
+
@@ -367,10 +368,10 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
491
|
+
>
|
|
492
|
+
<box>
|
|
493
|
+
<Show when={projectID() && worktrees.loading}>
|
|
494
|
+
- <Spinner color={theme.text.muted}>Loading worktrees…</Spinner>
|
|
495
|
+
+ <Spinner color={theme.text.muted}>{chromeLabel("Loading worktrees…")}</Spinner>
|
|
496
|
+
</Show>
|
|
497
|
+
<Show when={!projectID() && (recent.loading || projects.loading)}>
|
|
498
|
+
- <Spinner color={theme.text.muted}>Refreshing sessions and projects…</Spinner>
|
|
499
|
+
+ <Spinner color={theme.text.muted}>{chromeLabel("Refreshing sessions and projects…")}</Spinner>
|
|
500
|
+
</Show>
|
|
501
|
+
<Show when={!projectID() && (recent() === false || projects() === false)}>
|
|
502
|
+
<text fg={theme.text.feedback.error.base}>
|
|
503
|
+
@@ -422,12 +423,12 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
504
|
+
<text fg={theme.text.muted}>
|
|
505
|
+
{projectID()
|
|
506
|
+
? worktrees.loading
|
|
507
|
+
- ? "Loading worktrees…"
|
|
508
|
+
+ ? chromeLabel("Loading worktrees…")
|
|
509
|
+
: "No matching worktrees"
|
|
510
|
+
: recent.loading || projects.loading || matched.loading
|
|
511
|
+
- ? "Searching sessions and projects…"
|
|
512
|
+
+ ? chromeLabel("Searching sessions and projects…")
|
|
513
|
+
: shortcuts.get("session.list")
|
|
514
|
+
- ? `No matches · search all sessions with ${shortcuts.get("session.list")}`
|
|
515
|
+
+ ? `No matches ${chromeStatusSeparator()} search all sessions with ${shortcuts.get("session.list")}`
|
|
516
|
+
: "No matches"}
|
|
517
|
+
</text>
|
|
518
|
+
</box>
|
|
519
|
+
@@ -453,7 +454,7 @@ export function DialogOpen(props: { sessions: SessionInfo[]; onLoad: (sessions:
|
|
520
|
+
placeholder="Worktree name (optional)"
|
|
521
|
+
description={() => <text fg={theme.text.muted}>Leave blank for a random name.</text>}
|
|
522
|
+
busy={creating()}
|
|
523
|
+
- busyText="Creating worktree…"
|
|
524
|
+
+ busyText={chromeLabel("Creating worktree…")}
|
|
525
|
+
onCancel={cancelCreation}
|
|
526
|
+
onConfirm={(value) => {
|
|
527
|
+
const id = projectID()!
|
|
528
|
+
diff --git a/packages/tui/src/component/dialog-pair.tsx b/packages/tui/src/component/dialog-pair.tsx
|
|
529
|
+
index 0bc0ce44b49ee4e332cbdf001ae8b02764c93973..5e9d875450ea17d1636083216bb94950f22d36f8 100644
|
|
530
|
+
--- a/packages/tui/src/component/dialog-pair.tsx
|
|
531
|
+
+++ b/packages/tui/src/component/dialog-pair.tsx
|
|
532
|
+
@@ -7,6 +7,7 @@ import { useTheme } from "../context/theme"
|
|
533
|
+
import { DialogHeader, DialogLayout, useDialog } from "../ui/dialog"
|
|
534
|
+
import { Link } from "../ui/link"
|
|
535
|
+
import { errorMessage } from "../util/error"
|
|
536
|
+
+import { chromeLabel } from "../ui/glyphs"
|
|
537
|
+
|
|
538
|
+
export function DialogPair() {
|
|
539
|
+
const client = useClient()
|
|
540
|
+
@@ -91,7 +92,7 @@ export function DialogPair() {
|
|
541
|
+
<Show
|
|
542
|
+
when={loadError()}
|
|
543
|
+
fallback={
|
|
544
|
+
- <Show when={info()} fallback={<text fg={theme.text.muted}>Loading server information…</text>}>
|
|
545
|
+
+ <Show when={info()} fallback={<text fg={theme.text.muted}>{chromeLabel("Loading server information…")}</text>}>
|
|
546
|
+
<Show
|
|
547
|
+
when={dimensions().height >= 36}
|
|
548
|
+
fallback={
|
|
549
|
+
diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx
|
|
550
|
+
index d5fb5ac4f9b0cb717f649d4d43ed121f3aff66f6..5a59c284734c104248784af91bd8a4758682b6ac 100644
|
|
551
|
+
--- a/packages/tui/src/component/dialog-session-list.tsx
|
|
552
|
+
+++ b/packages/tui/src/component/dialog-session-list.tsx
|
|
553
|
+
@@ -23,6 +23,7 @@ import { useConfig, useNewDesigns } from "../config"
|
|
554
|
+
import { withTimestampedFallback } from "@opencode/util/session-title-fallback"
|
|
555
|
+
import { projectName } from "../util/project"
|
|
556
|
+
import { useLocation } from "../context/location"
|
|
557
|
+
+import { chromeEllipsis, chromeLabel } from "../ui/glyphs"
|
|
558
|
+
|
|
559
|
+
export function DialogSessionList() {
|
|
560
|
+
const dialog = useDialog()
|
|
561
|
+
@@ -111,7 +112,7 @@ export function DialogSessionList() {
|
|
562
|
+
const searchState = createMemo(() => {
|
|
563
|
+
const query = filter().trim()
|
|
564
|
+
if (query !== search().trim() || searchResults.loading)
|
|
565
|
+
- return { message: query ? "Searching sessions…" : "Loading sessions…", error: false }
|
|
566
|
+
+ return { message: query ? chromeLabel("Searching sessions…") : chromeLabel("Loading sessions…"), error: false }
|
|
567
|
+
const result = searchResults()
|
|
568
|
+
if (result?.query === query && result.error)
|
|
569
|
+
return {
|
|
570
|
+
@@ -155,10 +156,7 @@ export function DialogSessionList() {
|
|
571
|
+
const project = data.project.get(session.projectID)
|
|
572
|
+
const root = session.subpath ? path.resolve(directory, ...session.subpath.split("/").map(() => "..")) : directory
|
|
573
|
+
const relative = path.relative(project?.canonical ?? root, root)
|
|
574
|
+
- const footer =
|
|
575
|
+
- relative.startsWith("..") || path.isAbsolute(relative)
|
|
576
|
+
- ? Locale.truncate(path.basename(relative), 25)
|
|
577
|
+
- : undefined
|
|
578
|
+
+ const footer = sessionListRelativePathFooter(relative)
|
|
579
|
+
const slot = sessionTabs.enabled() ? undefined : slotByID.get(session.id)
|
|
580
|
+
const deleting = toDelete() === session.id
|
|
581
|
+
const attention = sessionTabs.status(session.id).attention
|
|
582
|
+
@@ -298,6 +296,12 @@ export function DialogSessionList() {
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
+export function sessionListRelativePathFooter(relative: string) {
|
|
587
|
+
+ return relative.startsWith("..") || path.isAbsolute(relative)
|
|
588
|
+
+ ? Locale.truncate(path.basename(relative), 25, chromeEllipsis())
|
|
589
|
+
+ : undefined
|
|
590
|
+
+}
|
|
591
|
+
+
|
|
592
|
+
function quickSwitchRange(first: string, last: string) {
|
|
593
|
+
const prefix = first.slice(0, -1)
|
|
594
|
+
if (first.endsWith("1") && last === `${prefix}9`) return `${prefix}1-9`
|
|
595
|
+
diff --git a/packages/tui/src/component/dialog-shell-output.tsx b/packages/tui/src/component/dialog-shell-output.tsx
|
|
596
|
+
index 42f0c98d1b0cd7e0f8244f71da4db6748e1621d3..4ba60790d24040227d4bcdc32e611330db8bb842 100644
|
|
597
|
+
--- a/packages/tui/src/component/dialog-shell-output.tsx
|
|
598
|
+
+++ b/packages/tui/src/component/dialog-shell-output.tsx
|
|
599
|
+
@@ -7,9 +7,14 @@ import { useClient } from "../context/client"
|
|
600
|
+
import { Keymap } from "../context/keymap"
|
|
601
|
+
import { useTheme } from "../context/theme"
|
|
602
|
+
import { DialogFooter, DialogHeader, DialogLayout, useDialog } from "../ui/dialog"
|
|
603
|
+
+import { chromeGlyph, chromeLabel } from "../ui/glyphs"
|
|
604
|
+
|
|
605
|
+
const PAGE_BYTES = 64 * 1024
|
|
606
|
+
|
|
607
|
+
+export function shellOutputOmittedLabel() {
|
|
608
|
+
+ return `Earlier output omitted ${chromeGlyph("·")} showing recent output`
|
|
609
|
+
+}
|
|
610
|
+
+
|
|
611
|
+
export function DialogShellOutput(props: { shell: ShellInfo; location: LocationRef }) {
|
|
612
|
+
const client = useClient()
|
|
613
|
+
const dialog = useDialog()
|
|
614
|
+
@@ -65,7 +70,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
|
|
615
|
+
.catch((cause: unknown) => {
|
|
616
|
+
if (disposed) return
|
|
617
|
+
missing = isShellNotFoundError(cause)
|
|
618
|
+
- setError(missing ? "Shell output is no longer available." : "Unable to read shell output. Retrying…")
|
|
619
|
+
+ setError(missing ? "Shell output is no longer available." : chromeLabel("Unable to read shell output. Retrying…"))
|
|
620
|
+
})
|
|
621
|
+
.then((more) => {
|
|
622
|
+
// Poll only while the viewer is open, including after exit so the final
|
|
623
|
+
@@ -84,7 +89,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
|
|
624
|
+
if (info().status === "running") return "Running"
|
|
625
|
+
if (info().status === "timeout") return "Timed out"
|
|
626
|
+
if (info().status === "killed") return "Killed"
|
|
627
|
+
- return info().exit === undefined ? "Exited" : `Exited · code ${info().exit}`
|
|
628
|
+
+ return info().exit === undefined ? "Exited" : `Exited ${chromeGlyph("·")} code ${info().exit}`
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
Keymap.createLayer(() => ({
|
|
632
|
+
@@ -117,7 +122,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
|
|
633
|
+
<DialogFooter
|
|
634
|
+
actions={{
|
|
635
|
+
secondary: [
|
|
636
|
+
- { title: "scroll", key: "↑/↓" },
|
|
637
|
+
+ { title: "scroll", key: `${chromeGlyph("↑")}/${chromeGlyph("↓")}` },
|
|
638
|
+
{ title: "follow", key: "end" },
|
|
639
|
+
{ title: "back", key: "esc" },
|
|
640
|
+
],
|
|
641
|
+
@@ -130,7 +135,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
|
|
642
|
+
{props.shell.command}
|
|
643
|
+
</text>
|
|
644
|
+
<Show when={omitted()}>
|
|
645
|
+
- <text fg={theme.text.muted}>Earlier output omitted · showing recent output</text>
|
|
646
|
+
+ <text fg={theme.text.muted}>{shellOutputOmittedLabel()}</text>
|
|
647
|
+
</Show>
|
|
648
|
+
<scrollbox
|
|
649
|
+
id="shell-output-scroll"
|
|
650
|
+
@@ -143,7 +148,7 @@ export function DialogShellOutput(props: { shell: ShellInfo; location: LocationR
|
|
651
|
+
<text fg={theme.text.base} wrapMode="word">
|
|
652
|
+
{text() ||
|
|
653
|
+
(output() === undefined
|
|
654
|
+
- ? "Loading output…"
|
|
655
|
+
+ ? "Loading output..."
|
|
656
|
+
: "No captured output. Output redirected to files is not shown here.")}
|
|
657
|
+
</text>
|
|
658
|
+
</scrollbox>
|
|
659
|
+
diff --git a/packages/tui/src/component/dialog-skill.tsx b/packages/tui/src/component/dialog-skill.tsx
|
|
660
|
+
index 5e45e628b1d72178fd6574922d0ef4016bb2b8f3..6d3d8ebb6cb6c0461f9789ae712b2dbdf0fa2435 100644
|
|
661
|
+
--- a/packages/tui/src/component/dialog-skill.tsx
|
|
662
|
+
+++ b/packages/tui/src/component/dialog-skill.tsx
|
|
663
|
+
@@ -6,6 +6,7 @@ import { useTheme } from "../context/theme"
|
|
664
|
+
import { errorMessage } from "../util/error"
|
|
665
|
+
import { useData } from "../context/data"
|
|
666
|
+
import type { LocationRef } from "@opencode/client"
|
|
667
|
+
+import { chromeLabel } from "../ui/glyphs"
|
|
668
|
+
|
|
669
|
+
export type DialogSkillProps = {
|
|
670
|
+
location?: LocationRef
|
|
671
|
+
@@ -78,7 +79,7 @@ export function DialogSkill(props: DialogSkillProps) {
|
|
672
|
+
</Match>
|
|
673
|
+
<Match when={skills.loading}>
|
|
674
|
+
<box paddingLeft={DIALOG_LIST_INSET} paddingRight={DIALOG_LIST_INSET}>
|
|
675
|
+
- <text fg={theme.text.muted}>Loading skills…</text>
|
|
676
|
+
+ <text fg={theme.text.muted}>{chromeLabel("Loading skills…")}</text>
|
|
677
|
+
</box>
|
|
678
|
+
</Match>
|
|
679
|
+
</Switch>
|
|
680
|
+
diff --git a/packages/tui/src/component/dialog-stash.tsx b/packages/tui/src/component/dialog-stash.tsx
|
|
681
|
+
index 4113c7fd969f4d79709b049cada13a3faf7b8832..f80c7e9f35da2f2bb82083f0da4ada1ca965be19 100644
|
|
682
|
+
--- a/packages/tui/src/component/dialog-stash.tsx
|
|
683
|
+
+++ b/packages/tui/src/component/dialog-stash.tsx
|
|
684
|
+
@@ -5,6 +5,7 @@ import { Locale } from "../util/locale"
|
|
685
|
+
import { Keymap } from "../context/keymap"
|
|
686
|
+
import { useTheme } from "../context/theme"
|
|
687
|
+
import { usePromptStash, type StashEntry } from "../prompt/stash"
|
|
688
|
+
+import { chromeEllipsis } from "../ui/glyphs"
|
|
689
|
+
|
|
690
|
+
function getRelativeTime(timestamp: number): string {
|
|
691
|
+
const now = Date.now()
|
|
692
|
+
@@ -23,7 +24,7 @@ function getRelativeTime(timestamp: number): string {
|
|
693
|
+
|
|
694
|
+
function getStashPreview(input: string, maxLength: number = 50): string {
|
|
695
|
+
const firstLine = input.split("\n")[0].trim()
|
|
696
|
+
- return Locale.truncate(firstLine, maxLength)
|
|
697
|
+
+ return Locale.truncate(firstLine, maxLength, chromeEllipsis())
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
|
|
701
|
+
diff --git a/packages/tui/src/component/dialog-status.tsx b/packages/tui/src/component/dialog-status.tsx
|
|
702
|
+
index f94f906cdf9d187234c24f575450d8ad13e3a559..2495eaf55ba47680be16e64a4110a99c0d11e116 100644
|
|
703
|
+
--- a/packages/tui/src/component/dialog-status.tsx
|
|
704
|
+
+++ b/packages/tui/src/component/dialog-status.tsx
|
|
705
|
+
@@ -2,6 +2,7 @@ import { useTheme } from "../context/theme"
|
|
706
|
+
import { DialogHeader, DialogLayout } from "../ui/dialog"
|
|
707
|
+
import { useData } from "../context/data"
|
|
708
|
+
import { For, Match, Switch, Show, createMemo } from "solid-js"
|
|
709
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
710
|
+
|
|
711
|
+
export function DialogStatus() {
|
|
712
|
+
const data = useData()
|
|
713
|
+
@@ -25,7 +26,7 @@ export function DialogStatus() {
|
|
714
|
+
{(item) => (
|
|
715
|
+
<box flexDirection="row" gap={1}>
|
|
716
|
+
<text flexShrink={0} style={{ fg: color(item.status.status) }}>
|
|
717
|
+
- •
|
|
718
|
+
+ {chromeGlyph("•")}
|
|
719
|
+
</text>
|
|
720
|
+
<text fg={theme.text.base} wrapMode="word">
|
|
721
|
+
<b>{item.name}</b>{" "}
|
|
722
|
+
diff --git a/packages/tui/src/component/dialog-update.tsx b/packages/tui/src/component/dialog-update.tsx
|
|
723
|
+
index 9dcf07b21402bea0548420e0b53cd84f08f90cc3..54469a0b69bdb306414819c50b65b52d4356b08d 100644
|
|
724
|
+
--- a/packages/tui/src/component/dialog-update.tsx
|
|
725
|
+
+++ b/packages/tui/src/component/dialog-update.tsx
|
|
726
|
+
@@ -6,6 +6,7 @@ import type { UpdateState } from "../context/update-notification"
|
|
727
|
+
import { DialogHeader, DialogLayout, useDialog } from "../ui/dialog"
|
|
728
|
+
import { errorMessage } from "../util/error"
|
|
729
|
+
import { Spinner } from "./spinner"
|
|
730
|
+
+import { chromeEllipsis, chromeLabel } from "../ui/glyphs"
|
|
731
|
+
|
|
732
|
+
export function DialogUpdate(props: {
|
|
733
|
+
check?: (signal: AbortSignal) => Promise<string | undefined>
|
|
734
|
+
@@ -133,7 +134,7 @@ export function DialogUpdate(props: {
|
|
735
|
+
{(current) => (
|
|
736
|
+
<Switch>
|
|
737
|
+
<Match when={current.type === "checking"}>
|
|
738
|
+
- <Spinner shimmer={theme.text.base}>Checking for updates…</Spinner>
|
|
739
|
+
+ <Spinner shimmer={theme.text.base}>{chromeLabel("Checking for updates…")}</Spinner>
|
|
740
|
+
</Match>
|
|
741
|
+
<Match when={current.type === "available"}>
|
|
742
|
+
<text fg={theme.text.muted}>
|
|
743
|
+
@@ -142,7 +143,7 @@ export function DialogUpdate(props: {
|
|
744
|
+
</Match>
|
|
745
|
+
<Match when={current.type === "installing"}>
|
|
746
|
+
<Spinner shimmer={theme.text.base}>
|
|
747
|
+
- {current.type === "installing" ? `Installing OpenCode ${current.version}…` : ""}
|
|
748
|
+
+ {current.type === "installing" ? `Installing OpenCode ${current.version}${chromeEllipsis()}` : ""}
|
|
749
|
+
</Spinner>
|
|
750
|
+
</Match>
|
|
751
|
+
<Match when={current.type === "installed"}>
|
|
752
|
+
diff --git a/packages/tui/src/component/dialog-workspaces.tsx b/packages/tui/src/component/dialog-workspaces.tsx
|
|
753
|
+
index 70aaa8ff4bf2da36c787e59235af509391030dbe..4f3a04971459af5e93b10268573629a5a0ebf165 100644
|
|
754
|
+
--- a/packages/tui/src/component/dialog-workspaces.tsx
|
|
755
|
+
+++ b/packages/tui/src/component/dialog-workspaces.tsx
|
|
756
|
+
@@ -15,6 +15,7 @@ import { errorMessage } from "../util/error"
|
|
757
|
+
import { isRecord } from "../util/record"
|
|
758
|
+
import { useToast } from "../ui/toast"
|
|
759
|
+
import { Spinner } from "./spinner"
|
|
760
|
+
+import { chromeEllipsis, chromeLabel } from "../ui/glyphs"
|
|
761
|
+
import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
|
|
762
|
+
import type { WorktreeListOutput } from "@opencode/client"
|
|
763
|
+
import { useRoute } from "../context/route"
|
|
764
|
+
@@ -168,7 +169,7 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
|
|
765
|
+
const title = abbreviateHome(item.location, paths.home)
|
|
766
|
+
const suffix =
|
|
767
|
+
item.location === item.root.directory ? undefined : path.sep + path.relative(item.root.directory, item.location)
|
|
768
|
+
- const visible = Locale.truncateLeft(title, titleWidth)
|
|
769
|
+
+ const visible = Locale.truncateLeft(title, titleWidth, chromeEllipsis())
|
|
770
|
+
const split = suffix ? Math.max(0, visible.length - suffix.length) : visible.length
|
|
771
|
+
const deleting = toDelete() === item.location
|
|
772
|
+
const isRemoving = removing() === item.location
|
|
773
|
+
@@ -350,7 +351,7 @@ export function DialogWorkspaces(props: DialogWorkspacesProps) {
|
|
774
|
+
</box>
|
|
775
|
+
) : directories.loading || loadedProject.loading ? (
|
|
776
|
+
<box paddingLeft={DIALOG_LIST_INSET} paddingRight={DIALOG_LIST_INSET}>
|
|
777
|
+
- <text fg={theme.text.muted}>Loading worktrees…</text>
|
|
778
|
+
+ <text fg={theme.text.muted}>{chromeLabel("Loading worktrees…")}</text>
|
|
779
|
+
</box>
|
|
780
|
+
) : (
|
|
781
|
+
<box paddingLeft={DIALOG_LIST_INSET} paddingRight={DIALOG_LIST_INSET}>
|
|
782
|
+
diff --git a/packages/tui/src/component/error-component.tsx b/packages/tui/src/component/error-component.tsx
|
|
783
|
+
index 48ad4b107d755a296266c89ceb7479b6bd771b18..b4e46b9d8ebc3396009e9093be88f71a6c4967bc 100644
|
|
784
|
+
--- a/packages/tui/src/component/error-component.tsx
|
|
785
|
+
+++ b/packages/tui/src/component/error-component.tsx
|
|
786
|
+
@@ -6,6 +6,7 @@ import { useClipboard } from "../context/clipboard"
|
|
787
|
+
import { useExit } from "../context/exit"
|
|
788
|
+
import { useTuiApp } from "../context/runtime"
|
|
789
|
+
import { describeOS, describeTerminal } from "../util/system"
|
|
790
|
+
+import { chromeBorderChars, chromeGlyph, chromeLabel } from "../ui/glyphs"
|
|
791
|
+
|
|
792
|
+
export function ErrorComponent(props: { error: Error; reset: () => void; mode?: "dark" | "light" }) {
|
|
793
|
+
const term = useTerminalDimensions()
|
|
794
|
+
@@ -55,7 +56,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
|
795
|
+
const actions = [
|
|
796
|
+
{
|
|
797
|
+
key: "c",
|
|
798
|
+
- label: () => ({ idle: "Copy report", copied: "✓ Copied", failed: "Copy failed" })[copyState()],
|
|
799
|
+
+ label: () => ({ idle: "Copy report", copied: `${chromeGlyph("✓")} Copied`, failed: "Copy failed" })[copyState()],
|
|
800
|
+
copy: true,
|
|
801
|
+
onUse: copyReport,
|
|
802
|
+
},
|
|
803
|
+
@@ -129,6 +130,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
|
804
|
+
flexShrink={0}
|
|
805
|
+
border
|
|
806
|
+
borderStyle="rounded"
|
|
807
|
+
+ customBorderChars={chromeBorderChars()}
|
|
808
|
+
borderColor={colors.error}
|
|
809
|
+
title=" Error "
|
|
810
|
+
titleColor={colors.error}
|
|
811
|
+
@@ -183,10 +185,11 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
|
812
|
+
minHeight={3}
|
|
813
|
+
border
|
|
814
|
+
borderStyle="rounded"
|
|
815
|
+
+ customBorderChars={chromeBorderChars()}
|
|
816
|
+
borderColor={colors.borderSubtle}
|
|
817
|
+
title=" Stack trace "
|
|
818
|
+
titleColor={colors.muted}
|
|
819
|
+
- bottomTitle=" ↑↓ scroll "
|
|
820
|
+
+ bottomTitle={` ${chromeGlyph("↑")}${chromeGlyph("↓")} scroll `}
|
|
821
|
+
bottomTitleAlignment="right"
|
|
822
|
+
paddingLeft={1}
|
|
823
|
+
paddingRight={1}
|
|
824
|
+
@@ -205,7 +208,7 @@ export function ErrorComponent(props: { error: Error; reset: () => void; mode?:
|
|
825
|
+
<box flexDirection="column" alignItems="center" flexShrink={0}>
|
|
826
|
+
<text fg={colors.muted}>
|
|
827
|
+
{copyState() === "copied"
|
|
828
|
+
- ? "Report copied — paste it into a new GitHub issue."
|
|
829
|
+
+ ? chromeLabel("Report copied — paste it into a new GitHub issue.")
|
|
830
|
+
: copyState() === "failed"
|
|
831
|
+
? "Clipboard write failed. Try again or report the crash manually."
|
|
832
|
+
: "Copy the report and open a GitHub issue to help us fix this."}
|
|
833
|
+
@@ -237,7 +240,7 @@ function buildIssueURL(message: string, stack: string, version: string) {
|
|
834
|
+
// clipped trace is obvious. searchParams.set handles encoding without throwing,
|
|
835
|
+
// so measuring url.toString() is both correct and safe on any input.
|
|
836
|
+
const MAX_URL_LENGTH = 6000
|
|
837
|
+
- const marker = "\n… (truncated)"
|
|
838
|
+
+ const marker = `\n${chromeGlyph("…")} (truncated)`
|
|
839
|
+
const head = `The OpenCode TUI crashed with an unexpected error.\n\n**Error:** ${message}\n\n**Stack trace:**\n`
|
|
840
|
+
const setBody = (body: string) => url.searchParams.set("description", head + "```\n" + body + "\n```")
|
|
841
|
+
|
|
842
|
+
diff --git a/packages/tui/src/component/logo.tsx b/packages/tui/src/component/logo.tsx
|
|
843
|
+
index 2c3abc3cb0e8081ad654802d18e4eb93a08362a8..e9e4e3bf9567d423d68239c5157bda858ae530b4 100644
|
|
844
|
+
--- a/packages/tui/src/component/logo.tsx
|
|
845
|
+
+++ b/packages/tui/src/component/logo.tsx
|
|
846
|
+
@@ -4,10 +4,18 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|
847
|
+
import { useTheme } from "../context/theme"
|
|
848
|
+
import { tint } from "../theme/color"
|
|
849
|
+
import { go, logo } from "../logo"
|
|
850
|
+
+import { ASCII_LOGO, asciiGlyphs } from "../ui/glyphs"
|
|
851
|
+
|
|
852
|
+
export function Logo() {
|
|
853
|
+
const theme = useTheme()
|
|
854
|
+
const dimensions = useTerminalDimensions()
|
|
855
|
+
+ const artwork = () => {
|
|
856
|
+
+ if (!asciiGlyphs()) return { logo, go }
|
|
857
|
+
+ return {
|
|
858
|
+
+ logo: { left: ASCII_LOGO.left, right: ASCII_LOGO.right },
|
|
859
|
+
+ go: { left: ASCII_LOGO.compact, right: ASCII_LOGO.compact },
|
|
860
|
+
+ }
|
|
861
|
+
+ }
|
|
862
|
+
|
|
863
|
+
const renderLine = (line: string, fg: RGBA, bold: boolean): JSX.Element[] => {
|
|
864
|
+
const shadow = tint(theme.background.base, fg, 0.25)
|
|
865
|
+
@@ -52,24 +60,24 @@ export function Logo() {
|
|
866
|
+
return (
|
|
867
|
+
<box>
|
|
868
|
+
{dimensions().height < 12 ? null : dimensions().width < 22 ? (
|
|
869
|
+
- <For each={go.right.slice(1)}>
|
|
870
|
+
+ <For each={artwork().go.right.slice(1)}>
|
|
871
|
+
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
|
|
872
|
+
</For>
|
|
873
|
+
) : dimensions().width < 44 ? (
|
|
874
|
+
<>
|
|
875
|
+
- <For each={logo.left.slice(1)}>
|
|
876
|
+
+ <For each={artwork().logo.left.slice(1)}>
|
|
877
|
+
{(line) => <box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>}
|
|
878
|
+
</For>
|
|
879
|
+
- <For each={logo.right}>
|
|
880
|
+
+ <For each={artwork().logo.right}>
|
|
881
|
+
{(line) => <box flexDirection="row">{renderLine(line, theme.text.base, true)}</box>}
|
|
882
|
+
</For>
|
|
883
|
+
</>
|
|
884
|
+
) : (
|
|
885
|
+
- <For each={logo.left}>
|
|
886
|
+
+ <For each={artwork().logo.left}>
|
|
887
|
+
{(line, index) => (
|
|
888
|
+
<box flexDirection="row" gap={1}>
|
|
889
|
+
<box flexDirection="row">{renderLine(line, theme.text.muted, false)}</box>
|
|
890
|
+
- <box flexDirection="row">{renderLine(logo.right[index()], theme.text.base, true)}</box>
|
|
891
|
+
+ <box flexDirection="row">{renderLine(artwork().logo.right[index()] ?? "", theme.text.base, true)}</box>
|
|
892
|
+
</box>
|
|
893
|
+
)}
|
|
894
|
+
</For>
|
|
895
|
+
diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx
|
|
896
|
+
index c669491c096edd5435ea8c976f1300282f35f334..a2b19c7ed3555c35d3b0ac2f1401760ed30eeee5 100644
|
|
897
|
+
--- a/packages/tui/src/component/prompt/autocomplete.tsx
|
|
898
|
+
+++ b/packages/tui/src/component/prompt/autocomplete.tsx
|
|
899
|
+
@@ -14,6 +14,7 @@ import { useConfig } from "../../config"
|
|
900
|
+
import { useLocation } from "../../context/location"
|
|
901
|
+
import { useTheme } from "../../context/theme"
|
|
902
|
+
import { SplitBorder } from "../../ui/border"
|
|
903
|
+
+import { chromeEllipsis, chromeLabel } from "../../ui/glyphs"
|
|
904
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
905
|
+
import { Locale } from "../../util/locale"
|
|
906
|
+
import type { PromptInfo, PromptPartRef } from "../../prompt/history"
|
|
907
|
+
@@ -345,7 +346,7 @@ export function Autocomplete(props: {
|
|
908
|
+
: { options: [], failed: true, mode: input.visible, query: input.query, resolved: false }
|
|
909
|
+
return {
|
|
910
|
+
options: result.map((item) => ({
|
|
911
|
+
- display: Locale.truncateMiddle(item.value, width),
|
|
912
|
+
+ display: Locale.truncateMiddle(item.value, width, chromeEllipsis()),
|
|
913
|
+
value: item.value,
|
|
914
|
+
isDirectory: true,
|
|
915
|
+
path: item.value,
|
|
916
|
+
@@ -374,7 +375,7 @@ export function Autocomplete(props: {
|
|
917
|
+
...result.data.map((item): AutocompleteOption => {
|
|
918
|
+
const { filename, part } = createFilePart(item, path.join(result.location.directory, item.path), lineRange)
|
|
919
|
+
return {
|
|
920
|
+
- display: Locale.truncateMiddle(filename, width),
|
|
921
|
+
+ display: Locale.truncateMiddle(filename, width, chromeEllipsis()),
|
|
922
|
+
value: filename,
|
|
923
|
+
isDirectory: item.type === "directory",
|
|
924
|
+
path: item.path,
|
|
925
|
+
@@ -511,7 +512,7 @@ export function Autocomplete(props: {
|
|
926
|
+
const value = item.value
|
|
927
|
+
return {
|
|
928
|
+
...item,
|
|
929
|
+
- display: Locale.truncateMiddle(item.display, width),
|
|
930
|
+
+ display: Locale.truncateMiddle(item.display, width, chromeEllipsis()),
|
|
931
|
+
onSelect: item.onSelect ?? (value ? () => insertDirectory(value) : undefined),
|
|
932
|
+
}
|
|
933
|
+
})
|
|
934
|
+
@@ -833,11 +834,11 @@ export function Autocomplete(props: {
|
|
935
|
+
const fileSearch = visibleFiles()
|
|
936
|
+
if (store.visible === "command") return "No matching commands"
|
|
937
|
+
if (store.visible === "directory") {
|
|
938
|
+
- if (files.loading) return "Searching…"
|
|
939
|
+
+ if (files.loading) return chromeLabel("Searching…")
|
|
940
|
+
if (fileSearch.failed) return "Could not search directories. Keep typing to try again."
|
|
941
|
+
return "No matching directories"
|
|
942
|
+
}
|
|
943
|
+
- if (files.loading) return "Searching…"
|
|
944
|
+
+ if (files.loading) return chromeLabel("Searching…")
|
|
945
|
+
if (fileSearch.failed) return "Could not search files. Keep typing to try again."
|
|
946
|
+
return "No matching files, agents, or references"
|
|
947
|
+
})
|
|
948
|
+
@@ -922,6 +923,7 @@ export function Autocomplete(props: {
|
|
949
|
+
{Locale.truncateMiddle(
|
|
950
|
+
confirmingAction() ? (destructive()?.confirm ?? "") : option().display,
|
|
951
|
+
contentWidth(),
|
|
952
|
+
+ chromeEllipsis(),
|
|
953
|
+
)}
|
|
954
|
+
</text>
|
|
955
|
+
<Show when={!confirmingAction() && option().description}>
|
|
956
|
+
diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
|
|
957
|
+
index a9f699c08ad2c2d50da1ae8b8a47c1997dc512b8..3cce36c2390c3de084411cf822d1eb6211893f0f 100644
|
|
958
|
+
--- a/packages/tui/src/component/prompt/index.tsx
|
|
959
|
+
+++ b/packages/tui/src/component/prompt/index.tsx
|
|
960
|
+
@@ -14,6 +14,7 @@ import { useTheme, useThemes } from "../../context/theme"
|
|
961
|
+
import { tint } from "../../theme/color"
|
|
962
|
+
import { createAnimatable, tween } from "../../ui/animation"
|
|
963
|
+
import { EmptyBorder, SplitBorder } from "../../ui/border"
|
|
964
|
+
+import { asciiGlyphs, chromeEllipsis, chromeGlyph, chromeLabel } from "../../ui/glyphs"
|
|
965
|
+
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
|
966
|
+
import { useClipboard } from "../../context/clipboard"
|
|
967
|
+
import { Spinner } from "../spinner"
|
|
968
|
+
@@ -249,7 +250,7 @@ export function Prompt(props: PromptProps) {
|
|
969
|
+
const editorFileLabelDisplay = createMemo(() => {
|
|
970
|
+
const file = editorFileLabel()
|
|
971
|
+
if (!file) return
|
|
972
|
+
- return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))))
|
|
973
|
+
+ return Locale.truncateMiddle(file, Math.max(12, Math.min(48, Math.floor(dimensions().width / 3))), chromeEllipsis())
|
|
974
|
+
})
|
|
975
|
+
const editorContextLabelState = createMemo(() => editor.labelState())
|
|
976
|
+
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
|
977
|
+
@@ -1595,10 +1596,10 @@ export function Prompt(props: PromptProps) {
|
|
978
|
+
const value = (() => {
|
|
979
|
+
if (store.mode === "shell") {
|
|
980
|
+
if (!shell().length) return undefined
|
|
981
|
+
- return `Run a command… "${shell()[store.placeholder % shell().length]}"`
|
|
982
|
+
+ return `${chromeLabel("Run a command…")} "${shell()[store.placeholder % shell().length]}"`
|
|
983
|
+
}
|
|
984
|
+
if (!list().length) return undefined
|
|
985
|
+
- return `Ask anything… "${list()[store.placeholder % list().length]}"`
|
|
986
|
+
+ return `${chromeLabel("Ask anything…")} "${list()[store.placeholder % list().length]}"`
|
|
987
|
+
})()
|
|
988
|
+
if (!value) return undefined
|
|
989
|
+
const width = dimensions().width < 44 ? dimensions().width - 5 : Math.min(75, dimensions().width - 4) - 5
|
|
990
|
+
@@ -1663,7 +1664,7 @@ export function Prompt(props: PromptProps) {
|
|
991
|
+
borderColor={borderHighlight()}
|
|
992
|
+
customBorderChars={{
|
|
993
|
+
...SplitBorder.customBorderChars,
|
|
994
|
+
- bottomLeft: "╹",
|
|
995
|
+
+ bottomLeft: asciiGlyphs() ? "+" : "╹",
|
|
996
|
+
}}
|
|
997
|
+
>
|
|
998
|
+
<box
|
|
999
|
+
@@ -1854,7 +1855,7 @@ export function Prompt(props: PromptProps) {
|
|
1000
|
+
borderColor={borderHighlight()}
|
|
1001
|
+
customBorderChars={{
|
|
1002
|
+
...EmptyBorder,
|
|
1003
|
+
- vertical: promptBg().a !== 0 ? "╹" : " ",
|
|
1004
|
+
+ vertical: promptBg().a !== 0 ? (asciiGlyphs() ? "|" : "╹") : " ",
|
|
1005
|
+
}}
|
|
1006
|
+
>
|
|
1007
|
+
<box
|
|
1008
|
+
@@ -1865,7 +1866,7 @@ export function Prompt(props: PromptProps) {
|
|
1009
|
+
promptBg().a !== 0
|
|
1010
|
+
? {
|
|
1011
|
+
...EmptyBorder,
|
|
1012
|
+
- horizontal: "▀",
|
|
1013
|
+
+ horizontal: asciiGlyphs() ? "-" : "▀",
|
|
1014
|
+
}
|
|
1015
|
+
: {
|
|
1016
|
+
...EmptyBorder,
|
|
1017
|
+
@@ -1890,7 +1891,7 @@ export function Prompt(props: PromptProps) {
|
|
1018
|
+
<Match when={status() === "running"}>
|
|
1019
|
+
<box flexDirection="row" gap={1} flexGrow={1} justifyContent="flex-start">
|
|
1020
|
+
<box marginLeft={1}>
|
|
1021
|
+
- <Show when={config.animations ?? true} fallback={<text fg={theme.text.muted}>[⋯]</text>}>
|
|
1022
|
+
+ <Show when={config.animations ?? true} fallback={<text fg={theme.text.muted}>[{chromeGlyph("…")}]</text>}>
|
|
1023
|
+
<spinner color={spinnerDef().color} frames={spinnerDef().frames} interval={40} />
|
|
1024
|
+
</Show>
|
|
1025
|
+
</box>
|
|
1026
|
+
diff --git a/packages/tui/src/component/prompt/metadata.tsx b/packages/tui/src/component/prompt/metadata.tsx
|
|
1027
|
+
index 60c47397dcc7856912e2ed5cd4bdeb0a6a31692c..d9fddeb91e9a6bb6dec65711652b6ce2579e9b21 100644
|
|
1028
|
+
--- a/packages/tui/src/component/prompt/metadata.tsx
|
|
1029
|
+
+++ b/packages/tui/src/component/prompt/metadata.tsx
|
|
1030
|
+
@@ -2,6 +2,7 @@ import { RGBA, TextAttributes } from "@opentui/core"
|
|
1031
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
1032
|
+
import { createMemo, Show } from "solid-js"
|
|
1033
|
+
import { useTheme } from "../../context/theme"
|
|
1034
|
+
+import { chromeEllipsis, chromeGlyph } from "../../ui/glyphs"
|
|
1035
|
+
import { Locale } from "../../util/locale"
|
|
1036
|
+
import { stringWidth } from "../../util/string-width"
|
|
1037
|
+
|
|
1038
|
+
@@ -48,7 +49,7 @@ export function PromptMetadataRow(props: {
|
|
1039
|
+
<Show when={props.mode === "normal" && layout().model}>
|
|
1040
|
+
<box flexDirection="row" gap={1} flexGrow={1} flexShrink={1} minWidth={0}>
|
|
1041
|
+
<Show when={layout().agent}>
|
|
1042
|
+
- <text fg={fade(theme.text.muted, props.modelAlpha)}>·</text>
|
|
1043
|
+
+ <text fg={fade(theme.text.muted, props.modelAlpha)}>{chromeGlyph("·")}</text>
|
|
1044
|
+
</Show>
|
|
1045
|
+
<text
|
|
1046
|
+
flexShrink={1}
|
|
1047
|
+
@@ -69,7 +70,7 @@ export function PromptMetadataRow(props: {
|
|
1048
|
+
<Show when={layout().variant}>
|
|
1049
|
+
{(variant) => (
|
|
1050
|
+
<>
|
|
1051
|
+
- <text fg={fade(theme.text.muted, props.variantAlpha)}>·</text>
|
|
1052
|
+
+ <text fg={fade(theme.text.muted, props.variantAlpha)}>{chromeGlyph("·")}</text>
|
|
1053
|
+
<text
|
|
1054
|
+
fg={fade(theme.text.feedback.warning.base, props.variantAlpha)}
|
|
1055
|
+
attributes={TextAttributes.BOLD}
|
|
1056
|
+
@@ -98,7 +99,7 @@ type Layout = {
|
|
1057
|
+
variant?: string
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
-function promptMetadataLayout(input: {
|
|
1061
|
+
+export function promptMetadataLayout(input: {
|
|
1062
|
+
width: number
|
|
1063
|
+
terminalWidth: number
|
|
1064
|
+
agent: string
|
|
1065
|
+
@@ -123,14 +124,16 @@ function promptMetadataLayout(input: {
|
|
1066
|
+
const fit = candidates.find((candidate) => stringWidth(text(candidate)) <= input.width)
|
|
1067
|
+
if (fit) return fit
|
|
1068
|
+
|
|
1069
|
+
- const prefix = agent ? `${agent} · ` : ""
|
|
1070
|
+
- const suffix = input.variant ? ` · ${input.variant}` : ""
|
|
1071
|
+
+ const separator = chromeGlyph("·")
|
|
1072
|
+
+ const prefix = agent ? `${agent} ${separator} ` : ""
|
|
1073
|
+
+ const suffix = input.variant ? ` ${separator} ` : ""
|
|
1074
|
+
return {
|
|
1075
|
+
agent,
|
|
1076
|
+
model: Locale.truncateWidth(
|
|
1077
|
+
input.model,
|
|
1078
|
+
Math.max(9, input.width - stringWidth(prefix) - stringWidth(suffix)),
|
|
1079
|
+
- ).replace(/\s+…$/, "…"),
|
|
1080
|
+
+ chromeEllipsis(),
|
|
1081
|
+
+ ),
|
|
1082
|
+
variant: input.variant,
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
@@ -139,8 +142,8 @@ function text(input: Layout) {
|
|
1086
|
+
return [
|
|
1087
|
+
...(input.agent ? [input.agent] : []),
|
|
1088
|
+
...(input.auto ? ["auto"] : []),
|
|
1089
|
+
- ...(input.model ? [...(input.agent ? ["·"] : []), input.model] : []),
|
|
1090
|
+
+ ...(input.model ? [...(input.agent ? [chromeGlyph("·")] : []), input.model] : []),
|
|
1091
|
+
...(input.provider ? [input.provider] : []),
|
|
1092
|
+
- ...(input.variant ? ["·", input.variant] : []),
|
|
1093
|
+
+ ...(input.variant ? [chromeGlyph("·"), input.variant] : []),
|
|
1094
|
+
].join(" ")
|
|
1095
|
+
}
|
|
1096
|
+
diff --git a/packages/tui/src/component/reconnecting.tsx b/packages/tui/src/component/reconnecting.tsx
|
|
1097
|
+
index 773eadaaeb1b5e1d0889ba032ae7e4edb4ea76ee..f612976132bba1d17585a6cbadd5c7c01e637757 100644
|
|
1098
|
+
--- a/packages/tui/src/component/reconnecting.tsx
|
|
1099
|
+
+++ b/packages/tui/src/component/reconnecting.tsx
|
|
1100
|
+
@@ -1,6 +1,7 @@
|
|
1101
|
+
import { RGBA } from "@opentui/core"
|
|
1102
|
+
import { useTheme } from "../context/theme"
|
|
1103
|
+
import { Spinner } from "./spinner"
|
|
1104
|
+
+import { chromeLabel } from "../ui/glyphs"
|
|
1105
|
+
|
|
1106
|
+
export function Reconnecting(props: { managed?: boolean }) {
|
|
1107
|
+
const theme = useTheme()
|
|
1108
|
+
@@ -28,7 +29,9 @@ export function Reconnecting(props: { managed?: boolean }) {
|
|
1109
|
+
paddingRight={2}
|
|
1110
|
+
gap={1}
|
|
1111
|
+
>
|
|
1112
|
+
- <Spinner color={theme.text.base}>{props.managed ? "Restarting service…" : "Connection lost…"}</Spinner>
|
|
1113
|
+
+ <Spinner color={theme.text.base}>
|
|
1114
|
+
+ {props.managed ? chromeLabel("Restarting service…") : chromeLabel("Connection lost…")}
|
|
1115
|
+
+ </Spinner>
|
|
1116
|
+
<text fg={theme.text.muted}>
|
|
1117
|
+
{props.managed
|
|
1118
|
+
? "Your session will resume automatically."
|
|
1119
|
+
diff --git a/packages/tui/src/component/session-tabs-rail.tsx b/packages/tui/src/component/session-tabs-rail.tsx
|
|
1120
|
+
index 8ecaa95787e844368671800ac787ed9bd2f53852..65c0626d6a035797712d2a7bc26bb0db920634af 100644
|
|
1121
|
+
--- a/packages/tui/src/component/session-tabs-rail.tsx
|
|
1122
|
+
+++ b/packages/tui/src/component/session-tabs-rail.tsx
|
|
1123
|
+
@@ -2,6 +2,7 @@ import { createMemo, createSignal } from "solid-js"
|
|
1124
|
+
import type { RGBA } from "@opentui/core"
|
|
1125
|
+
import { Keymap } from "../context/keymap"
|
|
1126
|
+
import { useTheme } from "../context/theme"
|
|
1127
|
+
+import { asciiGlyphs, chromeGlyph } from "../ui/glyphs"
|
|
1128
|
+
import { tint } from "../theme/color"
|
|
1129
|
+
import type { SessionTabsController } from "./session-tabs"
|
|
1130
|
+
|
|
1131
|
+
@@ -48,7 +49,7 @@ export function SessionTabsRailControls(props: {
|
|
1132
|
+
onMouseDragEnd={() => (pressed = false)}
|
|
1133
|
+
>
|
|
1134
|
+
<text width={1} height={1} fg={theme.text.action.secondary.base} selectable={false} wrapMode="none">
|
|
1135
|
+
- ⌕
|
|
1136
|
+
+ {chromeGlyph("⌕")}
|
|
1137
|
+
</text>
|
|
1138
|
+
</box>
|
|
1139
|
+
<text
|
|
1140
|
+
@@ -62,7 +63,7 @@ export function SessionTabsRailControls(props: {
|
|
1141
|
+
bg={hovered() && props.belowHighlighted ? hoverColor() : theme.background.raised.base}
|
|
1142
|
+
selectable={false}
|
|
1143
|
+
>
|
|
1144
|
+
- {(hovered() ? "▀" : props.belowHighlighted ? "▄" : " ").repeat(props.width)}
|
|
1145
|
+
+ {(asciiGlyphs() ? "-" : hovered() ? "▀" : props.belowHighlighted ? "▄" : " ").repeat(props.width)}
|
|
1146
|
+
</text>
|
|
1147
|
+
</box>
|
|
1148
|
+
)
|
|
1149
|
+
@@ -88,7 +89,7 @@ export function SessionTabHalfRow(props: {
|
|
1150
|
+
selectable={false}
|
|
1151
|
+
wrapMode="none"
|
|
1152
|
+
>
|
|
1153
|
+
- {(props.edge === "top" ? "▄" : "▀").repeat(props.width)}
|
|
1154
|
+
+ {(asciiGlyphs() ? "-" : props.edge === "top" ? "▄" : "▀").repeat(props.width)}
|
|
1155
|
+
</text>
|
|
1156
|
+
)
|
|
1157
|
+
}
|
|
1158
|
+
diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx
|
|
1159
|
+
index fdd23a43cbd4c3e318f211b7a8dc4fe2ab816fc1..540f63463e6e3120d6c7b374b4a545d2bf0b8092 100644
|
|
1160
|
+
--- a/packages/tui/src/component/session-tabs.tsx
|
|
1161
|
+
+++ b/packages/tui/src/component/session-tabs.tsx
|
|
1162
|
+
@@ -51,6 +51,7 @@ import { DialogSessionRename } from "./dialog-session-rename"
|
|
1163
|
+
import { Keymap } from "../context/keymap"
|
|
1164
|
+
import { registerOpencodeSpinner } from "./register-spinner"
|
|
1165
|
+
import { SPINNER_FRAMES } from "./spinner-frames"
|
|
1166
|
+
+import { asciiGlyphs, chromeEllipsis, chromeGlyph } from "../ui/glyphs"
|
|
1167
|
+
import { SessionTabsRailControls, SessionTabHalfRow } from "./session-tabs-rail"
|
|
1168
|
+
import "./title-shimmer"
|
|
1169
|
+
|
|
1170
|
+
@@ -58,8 +59,14 @@ registerOpencodeSpinner()
|
|
1171
|
+
|
|
1172
|
+
export const TAB_SPINNERS = {
|
|
1173
|
+
dots: { frames: SPINNER_FRAMES, interval: 80 },
|
|
1174
|
+
- arcs: { frames: ["◜", "◝", "◞", "◟"], interval: 120 },
|
|
1175
|
+
- quadrants: { frames: ["◴", "◷", "◶", "◵"], interval: 120 },
|
|
1176
|
+
+ arcs: {
|
|
1177
|
+
+ frames: asciiGlyphs() ? ["/", "\\", "\\", "/"] : ["◜", "◝", "◞", "◟"],
|
|
1178
|
+
+ interval: 120,
|
|
1179
|
+
+ },
|
|
1180
|
+
+ quadrants: {
|
|
1181
|
+
+ frames: asciiGlyphs() ? ["|", "/", "-", "\\"] : ["◴", "◷", "◶", "◵"],
|
|
1182
|
+
+ interval: 120,
|
|
1183
|
+
+ },
|
|
1184
|
+
line: { frames: ["|", "/", "-", "\\"], interval: 120 },
|
|
1185
|
+
}
|
|
1186
|
+
export type TabSpinner = keyof typeof TAB_SPINNERS
|
|
1187
|
+
@@ -71,6 +78,9 @@ export const TAB_UNREAD_MARKERS = {
|
|
1188
|
+
"large-square": "■",
|
|
1189
|
+
}
|
|
1190
|
+
export type TabUnreadMarker = keyof typeof TAB_UNREAD_MARKERS
|
|
1191
|
+
+export function tabUnreadMarker(marker: TabUnreadMarker = "small-dot") {
|
|
1192
|
+
+ return chromeGlyph(TAB_UNREAD_MARKERS[marker])
|
|
1193
|
+
+}
|
|
1194
|
+
|
|
1195
|
+
// A long title fades out over its last cells instead of cutting hard.
|
|
1196
|
+
const FADE_WIDTH = 4
|
|
1197
|
+
@@ -170,14 +180,16 @@ function TabIndicator(props: {
|
|
1198
|
+
const flash = Math.max(0, 1 - Math.abs(opacity - 0.8) / 0.2)
|
|
1199
|
+
return tint(props.backgroundColor, tint(unreadColor(), props.flashColor, flash * 0.3), Math.min(1, opacity / 0.8))
|
|
1200
|
+
}
|
|
1201
|
+
- const spinner = () => TAB_SPINNERS[props.spinner ?? "dots"]
|
|
1202
|
+
+ const spinner = () => {
|
|
1203
|
+
+ return TAB_SPINNERS[props.spinner ?? "dots"]
|
|
1204
|
+
+ }
|
|
1205
|
+
const label = () => {
|
|
1206
|
+
if (props.numbers) return props.label
|
|
1207
|
+
if (props.status.attention === "permission") return "!"
|
|
1208
|
+
if (props.status.attention === "question") return "?"
|
|
1209
|
+
if (runs()) return spinner().frames[0]
|
|
1210
|
+
if (props.label === "+") return "+"
|
|
1211
|
+
- if (props.status.unread || fading()) return TAB_UNREAD_MARKERS[props.unreadMarker ?? "small-dot"]
|
|
1212
|
+
+ if (props.status.unread || fading()) return tabUnreadMarker(props.unreadMarker)
|
|
1213
|
+
return props.idleLabel ?? ""
|
|
1214
|
+
}
|
|
1215
|
+
return (
|
|
1216
|
+
@@ -1106,7 +1118,7 @@ function VerticalSessionTabs(props: {
|
|
1217
|
+
tabs.close(tab.sessionID)
|
|
1218
|
+
}}
|
|
1219
|
+
>
|
|
1220
|
+
- {hovered() === tab.sessionID ? "✕" : ""}
|
|
1221
|
+
+ {hovered() === tab.sessionID ? chromeGlyph("✕") : ""}
|
|
1222
|
+
</text>
|
|
1223
|
+
</box>
|
|
1224
|
+
</box>
|
|
1225
|
+
@@ -1230,7 +1242,7 @@ function VerticalSessionTabs(props: {
|
|
1226
|
+
tabs.close()
|
|
1227
|
+
}}
|
|
1228
|
+
>
|
|
1229
|
+
- {addHovered() ? "×" : ""}
|
|
1230
|
+
+ {addHovered() ? chromeGlyph("×") : ""}
|
|
1231
|
+
</text>
|
|
1232
|
+
</Show>
|
|
1233
|
+
</box>
|
|
1234
|
+
@@ -1262,6 +1274,7 @@ function VerticalSessionTabs(props: {
|
|
1235
|
+
items().find((tab) => tab.sessionID === sessionID())?.title ??
|
|
1236
|
+
"Untitled session",
|
|
1237
|
+
tooltipWidth() - 2,
|
|
1238
|
+
+ chromeEllipsis(),
|
|
1239
|
+
)}
|
|
1240
|
+
</text>
|
|
1241
|
+
<text fg={theme.text.muted} wrapMode="none" selectable={false}>
|
|
1242
|
+
@@ -1556,7 +1569,7 @@ function HorizontalSessionTabs(props: {
|
|
1243
|
+
>
|
|
1244
|
+
<Show when={layout().before > 0}>
|
|
1245
|
+
<text width={sessionTabOverflowWidth(layout().before)} fg={theme.text.muted} selectable={false}>
|
|
1246
|
+
- ‹{layout().before}
|
|
1247
|
+
+ {chromeGlyph("‹")}{layout().before}
|
|
1248
|
+
</text>
|
|
1249
|
+
</Show>
|
|
1250
|
+
<For each={layout().tabs}>
|
|
1251
|
+
@@ -1753,7 +1766,7 @@ function HorizontalSessionTabs(props: {
|
|
1252
|
+
tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
|
|
1253
|
+
}}
|
|
1254
|
+
>
|
|
1255
|
+
- {hovered() === tab.sessionID ? "✕" : ""}
|
|
1256
|
+
+ {hovered() === tab.sessionID ? chromeGlyph("✕") : ""}
|
|
1257
|
+
</text>
|
|
1258
|
+
</box>
|
|
1259
|
+
</box>
|
|
1260
|
+
@@ -1762,7 +1775,7 @@ function HorizontalSessionTabs(props: {
|
|
1261
|
+
</For>
|
|
1262
|
+
<Show when={layout().after > 0}>
|
|
1263
|
+
<text width={sessionTabOverflowWidth(layout().after)} fg={theme.text.muted} selectable={false}>
|
|
1264
|
+
- {" " + layout().after}›
|
|
1265
|
+
+ {" " + layout().after}{chromeGlyph("›")}
|
|
1266
|
+
</text>
|
|
1267
|
+
</Show>
|
|
1268
|
+
<Show when={showPlus()}>
|
|
1269
|
+
diff --git a/packages/tui/src/component/spinner-frames.ts b/packages/tui/src/component/spinner-frames.ts
|
|
1270
|
+
index dca99b642a727b9846b0bb7b2b995cd2c7636683..3869068d90958c89ea97f8e3e4caccb6cf490db9 100644
|
|
1271
|
+
--- a/packages/tui/src/component/spinner-frames.ts
|
|
1272
|
+
+++ b/packages/tui/src/component/spinner-frames.ts
|
|
1273
|
+
@@ -1 +1,7 @@
|
|
1274
|
+
-export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
1275
|
+
+import { ASCII_SPINNER_FRAMES, asciiGlyphs } from "../ui/glyphs"
|
|
1276
|
+
+
|
|
1277
|
+
+const UNICODE_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
1278
|
+
+export function spinnerFrames() {
|
|
1279
|
+
+ return asciiGlyphs() ? ASCII_SPINNER_FRAMES : UNICODE_SPINNER_FRAMES
|
|
1280
|
+
+}
|
|
1281
|
+
+export const SPINNER_FRAMES = spinnerFrames()
|
|
1282
|
+
diff --git a/packages/tui/src/component/spinner.tsx b/packages/tui/src/component/spinner.tsx
|
|
1283
|
+
index 3207dcb6e4f4588297c24f88329e339d45df9b98..08a92ee9cfb22e8c94e61d36cedbc1a394b9c283 100644
|
|
1284
|
+
--- a/packages/tui/src/component/spinner.tsx
|
|
1285
|
+
+++ b/packages/tui/src/component/spinner.tsx
|
|
1286
|
+
@@ -6,6 +6,7 @@ import type { RGBA } from "@opentui/core"
|
|
1287
|
+
import { registerOpencodeSpinner } from "./register-spinner"
|
|
1288
|
+
import { SPINNER_FRAMES } from "./spinner-frames"
|
|
1289
|
+
import { ShimmerText } from "./shimmer-text"
|
|
1290
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
1291
|
+
|
|
1292
|
+
export { SPINNER_FRAMES } from "./spinner-frames"
|
|
1293
|
+
|
|
1294
|
+
@@ -26,7 +27,7 @@ export function Spinner(props: { children?: JSX.Element; color?: RGBA; shimmer?:
|
|
1295
|
+
return (
|
|
1296
|
+
<Show
|
|
1297
|
+
when={config.animations ?? true}
|
|
1298
|
+
- fallback={<text fg={color()}>{props.children ? <>⋯ {props.children}</> : "⋯"}</text>}
|
|
1299
|
+
+ fallback={<text fg={color()}>{props.children ? <>{chromeGlyph("…")} {props.children}</> : chromeGlyph("…")}</text>}
|
|
1300
|
+
>
|
|
1301
|
+
<Show
|
|
1302
|
+
when={props.shimmer}
|
|
1303
|
+
diff --git a/packages/tui/src/component/startup-loading.tsx b/packages/tui/src/component/startup-loading.tsx
|
|
1304
|
+
index 3df79782c63433021722986242d05dddd138a508..c1e437df95e8439f5fa82071e17cf8072a7d6157 100644
|
|
1305
|
+
--- a/packages/tui/src/component/startup-loading.tsx
|
|
1306
|
+
+++ b/packages/tui/src/component/startup-loading.tsx
|
|
1307
|
+
@@ -1,11 +1,14 @@
|
|
1308
|
+
import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js"
|
|
1309
|
+
import { useTheme } from "../context/theme"
|
|
1310
|
+
import { Spinner } from "./spinner"
|
|
1311
|
+
+import { chromeLabel } from "../ui/glyphs"
|
|
1312
|
+
|
|
1313
|
+
export function StartupLoading(props: { ready: () => boolean }) {
|
|
1314
|
+
const theme = useTheme()
|
|
1315
|
+
const [show, setShow] = createSignal(false)
|
|
1316
|
+
- const text = createMemo(() => (props.ready() ? "Finishing startup…" : "Loading plugins…"))
|
|
1317
|
+
+ const text = createMemo(() =>
|
|
1318
|
+
+ props.ready() ? chromeLabel("Finishing startup…") : chromeLabel("Loading plugins…"),
|
|
1319
|
+
+ )
|
|
1320
|
+
let wait: NodeJS.Timeout | undefined
|
|
1321
|
+
let hold: NodeJS.Timeout | undefined
|
|
1322
|
+
let stamp = 0
|
|
1323
|
+
diff --git a/packages/tui/src/component/tab-pulse.tsx b/packages/tui/src/component/tab-pulse.tsx
|
|
1324
|
+
index a6072fe109bde1a8eaf72b34c32208ce26806a9d..59e891b8682c0d2d3f9b580c5b09d19e5c274be7 100644
|
|
1325
|
+
--- a/packages/tui/src/component/tab-pulse.tsx
|
|
1326
|
+
+++ b/packages/tui/src/component/tab-pulse.tsx
|
|
1327
|
+
@@ -1,5 +1,6 @@
|
|
1328
|
+
import { OptimizedBuffer, Renderable, RGBA, type RenderableOptions, type RenderContext } from "@opentui/core"
|
|
1329
|
+
import { extend } from "@opentui/solid"
|
|
1330
|
+
+import { asciiGlyphs } from "../ui/glyphs"
|
|
1331
|
+
|
|
1332
|
+
type TabPulseOptions = RenderableOptions<TabPulseRenderable> & {
|
|
1333
|
+
edge?: "above" | "below"
|
|
1334
|
+
@@ -739,7 +740,7 @@ class TabPulseRenderable extends Renderable {
|
|
1335
|
+
buffer.setCell(
|
|
1336
|
+
this.screenX + index,
|
|
1337
|
+
this.screenY,
|
|
1338
|
+
- this._edge === "above" ? "▄" : "▀",
|
|
1339
|
+
+ asciiGlyphs() ? "-" : this._edge === "above" ? "▄" : "▀",
|
|
1340
|
+
this.renderColor,
|
|
1341
|
+
this.outerRenderColor,
|
|
1342
|
+
)
|
|
1343
|
+
diff --git a/packages/tui/src/context/keymap.tsx b/packages/tui/src/context/keymap.tsx
|
|
1344
|
+
index 521b3aeac86f97675ff7076c34a6ea278293db32..6d6ae30290ce6a6511b60dc3cb23523fbcdedf0e 100644
|
|
1345
|
+
--- a/packages/tui/src/context/keymap.tsx
|
|
1346
|
+
+++ b/packages/tui/src/context/keymap.tsx
|
|
1347
|
+
@@ -25,6 +25,7 @@ import {
|
|
1348
|
+
} from "solid-js"
|
|
1349
|
+
import { useConfig } from "../config"
|
|
1350
|
+
import { TuiKeybind } from "../config/keybind"
|
|
1351
|
+
+import { asciiGlyphs } from "../ui/glyphs"
|
|
1352
|
+
import { resolveInteractivity, useInteractivity } from "./interactivity"
|
|
1353
|
+
|
|
1354
|
+
declare module "@opentui/keymap" {
|
|
1355
|
+
@@ -459,10 +460,10 @@ function formatOptions(config: KeymapConfig) {
|
|
1356
|
+
leader: leader ? (typeof leader === "string" ? leader : stringifyKeyStroke(leader)) : TuiKeybind.LeaderDefault,
|
|
1357
|
+
},
|
|
1358
|
+
keyNameAliases: {
|
|
1359
|
+
- up: "↑",
|
|
1360
|
+
- down: "↓",
|
|
1361
|
+
- left: "←",
|
|
1362
|
+
- right: "→",
|
|
1363
|
+
+ up: asciiGlyphs() ? "up" : "↑",
|
|
1364
|
+
+ down: asciiGlyphs() ? "down" : "↓",
|
|
1365
|
+
+ left: asciiGlyphs() ? "left" : "←",
|
|
1366
|
+
+ right: asciiGlyphs() ? "right" : "→",
|
|
1367
|
+
pageup: "pgup",
|
|
1368
|
+
pagedown: "pgdn",
|
|
1369
|
+
delete: "del",
|
|
1370
|
+
diff --git a/packages/tui/src/context/session-tabs-model.ts b/packages/tui/src/context/session-tabs-model.ts
|
|
1371
|
+
index 856f9961e7fa8d7b880035f01c3c32a48551fffd..0a1356cedc938921f7dba1b985179a57b7848ca5 100644
|
|
1372
|
+
--- a/packages/tui/src/context/session-tabs-model.ts
|
|
1373
|
+
+++ b/packages/tui/src/context/session-tabs-model.ts
|
|
1374
|
+
@@ -1,3 +1,5 @@
|
|
1375
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
1376
|
+
+
|
|
1377
|
+
export type SessionTab = {
|
|
1378
|
+
sessionID: string
|
|
1379
|
+
title?: string
|
|
1380
|
+
@@ -18,7 +20,7 @@ export function sessionTabDetail(
|
|
1381
|
+
worktree: boolean,
|
|
1382
|
+
) {
|
|
1383
|
+
const branch = worktree && current !== defaultBranch ? current : undefined
|
|
1384
|
+
- return branch && project ? `${project} ⎇ ${branch}` : (branch ?? project)
|
|
1385
|
+
+ return branch && project ? `${project} ${chromeGlyph("⎇")} ${branch}` : (branch ?? project)
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
export type SessionTabHistory = {
|
|
1389
|
+
diff --git a/packages/tui/src/feature-plugins/home/footer.tsx b/packages/tui/src/feature-plugins/home/footer.tsx
|
|
1390
|
+
index d31888c2770f27bc7143edc258f6446407f8278e..eb939a1b51ff0fb5d7d69923b13de0fe04eba971 100644
|
|
1391
|
+
--- a/packages/tui/src/feature-plugins/home/footer.tsx
|
|
1392
|
+
+++ b/packages/tui/src/feature-plugins/home/footer.tsx
|
|
1393
|
+
@@ -1,6 +1,7 @@
|
|
1394
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
1395
|
+
import { createMemo, Match, Show, Switch } from "solid-js"
|
|
1396
|
+
import { useTerminalDimensions } from "@opentui/solid"
|
|
1397
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
1398
|
+
import { usePlugin } from "../../plugin/context"
|
|
1399
|
+
import { Slot } from "../../plugin/render"
|
|
1400
|
+
|
|
1401
|
+
@@ -25,7 +26,7 @@ function Mcp(props: { context: Plugin.Context }) {
|
|
1402
|
+
<text fg={props.context.theme.text.base}>
|
|
1403
|
+
<Switch>
|
|
1404
|
+
<Match when={failed()}>
|
|
1405
|
+
- <span style={{ fg: props.context.theme.text.feedback.error.base }}>⊙ </span>
|
|
1406
|
+
+ <span style={{ fg: props.context.theme.text.feedback.error.base }}>{chromeGlyph("⊙")} </span>
|
|
1407
|
+
{failed()} MCP failed
|
|
1408
|
+
</Match>
|
|
1409
|
+
<Match when={true}>
|
|
1410
|
+
@@ -35,7 +36,7 @@ function Mcp(props: { context: Plugin.Context }) {
|
|
1411
|
+
count() > 0 ? props.context.theme.text.feedback.success.base : props.context.theme.text.muted,
|
|
1412
|
+
}}
|
|
1413
|
+
>
|
|
1414
|
+
- ⊙{" "}
|
|
1415
|
+
+ {chromeGlyph("⊙")} {" "}
|
|
1416
|
+
</span>
|
|
1417
|
+
{count()} MCP
|
|
1418
|
+
</Match>
|
|
1419
|
+
@@ -63,7 +64,7 @@ function Plugins(props: { context: Plugin.Context }) {
|
|
1420
|
+
<Show when={failed()}>
|
|
1421
|
+
<box gap={1} flexDirection="row" flexShrink={0} onMouseUp={() => props.context.keymap.dispatch("plugins.list")}>
|
|
1422
|
+
<text fg={props.context.theme.text.base}>
|
|
1423
|
+
- <span style={{ fg: props.context.theme.text.feedback.error.base }}>⊙ </span>
|
|
1424
|
+
+ <span style={{ fg: props.context.theme.text.feedback.error.base }}>{chromeGlyph("⊙")} </span>
|
|
1425
|
+
{failed()} plugin{failed() === 1 ? "" : "s"} failed
|
|
1426
|
+
</text>
|
|
1427
|
+
<Show when={visibility().pluginCommand}>
|
|
1428
|
+
diff --git a/packages/tui/src/feature-plugins/prompt/btw.tsx b/packages/tui/src/feature-plugins/prompt/btw.tsx
|
|
1429
|
+
index 6aca5607022df6b4a68acdda644928e70698e41d..1cc488b4eacaef463d422230f4e6469c63005fa2 100644
|
|
1430
|
+
--- a/packages/tui/src/feature-plugins/prompt/btw.tsx
|
|
1431
|
+
+++ b/packages/tui/src/feature-plugins/prompt/btw.tsx
|
|
1432
|
+
@@ -3,6 +3,7 @@ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
|
1433
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
|
|
1434
|
+
import { createMemo, createSignal, Show } from "solid-js"
|
|
1435
|
+
import { Spinner } from "../../component/spinner"
|
|
1436
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
1437
|
+
import { useConfig } from "../../config"
|
|
1438
|
+
import { useClipboard } from "../../context/clipboard"
|
|
1439
|
+
import { Keymap } from "../../context/keymap"
|
|
1440
|
+
@@ -143,7 +144,7 @@ export function Answer(props: {
|
|
1441
|
+
actions: {
|
|
1442
|
+
primary: [
|
|
1443
|
+
copied()
|
|
1444
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: copy }
|
|
1445
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: copy }
|
|
1446
|
+
: { title: "copy response", key: "c", onTrigger: copy },
|
|
1447
|
+
],
|
|
1448
|
+
},
|
|
1449
|
+
@@ -153,9 +154,9 @@ export function Answer(props: {
|
|
1450
|
+
actions={{
|
|
1451
|
+
secondary: [
|
|
1452
|
+
copied()
|
|
1453
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: copy }
|
|
1454
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: copy }
|
|
1455
|
+
: { title: "copy", key: "c", onTrigger: copy },
|
|
1456
|
+
- { title: "scroll", key: "j/k ↑/↓" },
|
|
1457
|
+
+ { title: "scroll", key: `j/k ${chromeGlyph("↑")}/${chromeGlyph("↓")}` },
|
|
1458
|
+
],
|
|
1459
|
+
}}
|
|
1460
|
+
/>
|
|
1461
|
+
diff --git a/packages/tui/src/feature-plugins/prompt/footer.tsx b/packages/tui/src/feature-plugins/prompt/footer.tsx
|
|
1462
|
+
index f8fafdce21e086f156ead73edf1b3b88a1683b3f..b2bca2b046c74c8a7818a3b975679cb224856afc 100644
|
|
1463
|
+
--- a/packages/tui/src/feature-plugins/prompt/footer.tsx
|
|
1464
|
+
+++ b/packages/tui/src/feature-plugins/prompt/footer.tsx
|
|
1465
|
+
@@ -5,6 +5,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|
1466
|
+
import { TextAttributes } from "@opentui/core"
|
|
1467
|
+
import { useInteractivity } from "../../context/interactivity"
|
|
1468
|
+
import { stringWidth } from "../../util/string-width"
|
|
1469
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
1470
|
+
|
|
1471
|
+
const money = new Intl.NumberFormat("en-US", {
|
|
1472
|
+
style: "currency",
|
|
1473
|
+
@@ -83,15 +84,15 @@ export function PromptFooter(props: {
|
|
1474
|
+
{(value) => <span style={{ fg: props.context.theme.text.base }}>{value()} </span>}
|
|
1475
|
+
</Show>
|
|
1476
|
+
<Show when={subagents()}>{(value) => <>{value()}</>}</Show>
|
|
1477
|
+
- <Show when={subagents() && shells()}> · </Show>
|
|
1478
|
+
+ <Show when={subagents() && shells()}> {chromeGlyph("·")} </Show>
|
|
1479
|
+
<Show when={shells()}>{(value) => <>{value()}</>}</Show>
|
|
1480
|
+
</text>
|
|
1481
|
+
</box>
|
|
1482
|
+
</Show>
|
|
1483
|
+
<Show when={props.showDetails && layout().usage && status().length > 0}>
|
|
1484
|
+
<text fg={props.context.theme.text.muted} wrapMode="none" flexShrink={0}>
|
|
1485
|
+
- <Show when={live()}> · </Show>
|
|
1486
|
+
- {status().join(" · ")}
|
|
1487
|
+
+ <Show when={live()}> {chromeGlyph("·")} </Show>
|
|
1488
|
+
+ {joinPromptFooterStatus(status())}
|
|
1489
|
+
</text>
|
|
1490
|
+
</Show>
|
|
1491
|
+
</box>
|
|
1492
|
+
@@ -143,11 +144,16 @@ export default Plugin.define({
|
|
1493
|
+
},
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
-function promptFooterLayout(input: { width: number; usage: string[]; shortcuts: string[] }) {
|
|
1497
|
+
- const usage = input.usage.join(" · ")
|
|
1498
|
+
- const shortcuts = input.shortcuts.join(" · ")
|
|
1499
|
+
+export function joinPromptFooterStatus(status: string[]) {
|
|
1500
|
+
+ return status.join(` ${chromeGlyph("·")} `)
|
|
1501
|
+
+}
|
|
1502
|
+
+
|
|
1503
|
+
+export function promptFooterLayout(input: { width: number; usage: string[]; shortcuts: string[] }) {
|
|
1504
|
+
+ const separator = ` ${chromeGlyph("·")} `
|
|
1505
|
+
+ const usage = input.usage.join(separator)
|
|
1506
|
+
+ const shortcuts = input.shortcuts.join(separator)
|
|
1507
|
+
const available = Math.max(0, input.width - Math.min(28, Math.floor(input.width / 2)))
|
|
1508
|
+
- if (usage && shortcuts && stringWidth(`${usage} · ${shortcuts}`) <= available) {
|
|
1509
|
+
+ if (usage && shortcuts && stringWidth(`${usage}${separator}${shortcuts}`) <= available) {
|
|
1510
|
+
return { usage: true, shortcuts: true }
|
|
1511
|
+
}
|
|
1512
|
+
if (usage && stringWidth(usage) <= available) return { usage: true, shortcuts: false }
|
|
1513
|
+
diff --git a/packages/tui/src/feature-plugins/sidebar/footer.tsx b/packages/tui/src/feature-plugins/sidebar/footer.tsx
|
|
1514
|
+
index b875d5ffe803c6b8eefa50460c55ce5c762d4629..75609b21f7c57e53851ee95a10b42592a7103635 100644
|
|
1515
|
+
--- a/packages/tui/src/feature-plugins/sidebar/footer.tsx
|
|
1516
|
+
+++ b/packages/tui/src/feature-plugins/sidebar/footer.tsx
|
|
1517
|
+
@@ -5,6 +5,7 @@ import { FilePath } from "../../ui/file-path"
|
|
1518
|
+
import { useWorkingDirectoryActions } from "../../ui/working-directory-actions"
|
|
1519
|
+
import { usePromptMove } from "../../component/prompt/move"
|
|
1520
|
+
import { hasConnectedProvider } from "../../util/connected-provider"
|
|
1521
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
1522
|
+
|
|
1523
|
+
export function SidebarOnboarding(props: { context: Plugin.Context; sessionID: string }) {
|
|
1524
|
+
const dimensions = useTerminalDimensions()
|
|
1525
|
+
@@ -35,7 +36,7 @@ export function SidebarOnboarding(props: { context: Plugin.Context; sessionID: s
|
|
1526
|
+
gap={1}
|
|
1527
|
+
>
|
|
1528
|
+
<text flexShrink={0} fg={props.context.theme.text.base}>
|
|
1529
|
+
- ⬖
|
|
1530
|
+
+ {chromeGlyph("⬖")}
|
|
1531
|
+
</text>
|
|
1532
|
+
<box flexGrow={1} gap={1}>
|
|
1533
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
1534
|
+
@@ -51,7 +52,7 @@ export function SidebarOnboarding(props: { context: Plugin.Context; sessionID: s
|
|
1535
|
+
}).catch((error) => console.error("Failed to dismiss sidebar onboarding", error))
|
|
1536
|
+
}}
|
|
1537
|
+
>
|
|
1538
|
+
- ✕
|
|
1539
|
+
+ {chromeGlyph("✕")}
|
|
1540
|
+
</text>
|
|
1541
|
+
</box>
|
|
1542
|
+
<text fg={props.context.theme.text.muted}>OpenCode includes free models so you can start immediately.</text>
|
|
1543
|
+
diff --git a/packages/tui/src/feature-plugins/sidebar/mcp.tsx b/packages/tui/src/feature-plugins/sidebar/mcp.tsx
|
|
1544
|
+
index 6018ff0ccb4334db84a00ef6c35c37c4dbfbb393..f192f9ebe118e05ecdc02879f2b3e93c1dcf54f6 100644
|
|
1545
|
+
--- a/packages/tui/src/feature-plugins/sidebar/mcp.tsx
|
|
1546
|
+
+++ b/packages/tui/src/feature-plugins/sidebar/mcp.tsx
|
|
1547
|
+
@@ -1,6 +1,7 @@
|
|
1548
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
1549
|
+
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
|
1550
|
+
import { DialogMcp } from "../../component/dialog-mcp"
|
|
1551
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
1552
|
+
|
|
1553
|
+
export function SidebarMcp(props: { context: Plugin.Context; sessionID: string }) {
|
|
1554
|
+
const [view, updateView] = props.context.storage.store("view", { initial: { open: true } })
|
|
1555
|
+
@@ -34,7 +35,7 @@ export function SidebarMcp(props: { context: Plugin.Context; sessionID: string }
|
|
1556
|
+
}}
|
|
1557
|
+
>
|
|
1558
|
+
<Show when={list().length > 2}>
|
|
1559
|
+
- <text fg={theme.text.base}>{view.open ? "▼" : "▶"}</text>
|
|
1560
|
+
+ <text fg={theme.text.base}>{view.open ? chromeGlyph("▼") : chromeGlyph("▶")}</text>
|
|
1561
|
+
</Show>
|
|
1562
|
+
<text fg={theme.text.base}>
|
|
1563
|
+
<b>MCP</b>
|
|
1564
|
+
@@ -65,7 +66,7 @@ export function SidebarMcp(props: { context: Plugin.Context; sessionID: string }
|
|
1565
|
+
fg: dot(item.status.status),
|
|
1566
|
+
}}
|
|
1567
|
+
>
|
|
1568
|
+
- •
|
|
1569
|
+
+ {chromeGlyph("•")}
|
|
1570
|
+
</text>
|
|
1571
|
+
<text fg={theme.text.base} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
|
|
1572
|
+
<b>{item.name}</b>
|
|
1573
|
+
diff --git a/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx b/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx
|
|
1574
|
+
index 4a2c4dc06bc5be04057a97d152c2f07e2834f38c..7d0a7d4df510cf5001f4dca6b6a03e2f11241251 100644
|
|
1575
|
+
--- a/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx
|
|
1576
|
+
+++ b/packages/tui/src/feature-plugins/system/diff-viewer-file-tree.tsx
|
|
1577
|
+
@@ -3,6 +3,7 @@ import { MouseButton, TextAttributes, type MouseEvent, type ScrollBoxRenderable
|
|
1578
|
+
import { truncateFilePath } from "../../ui/file-path"
|
|
1579
|
+
import { stringWidth } from "../../util/string-width"
|
|
1580
|
+
import { useTheme } from "../../context/theme"
|
|
1581
|
+
+import { chromeGlyph, chromeEllipsis } from "../../ui/glyphs"
|
|
1582
|
+
import { tint } from "../../theme/color"
|
|
1583
|
+
import { createEffect, createMemo, createSignal, For, Match, Show, Switch, type JSX } from "solid-js"
|
|
1584
|
+
import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils"
|
|
1585
|
+
@@ -97,7 +98,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|
1586
|
+
</text>
|
|
1587
|
+
<Show when={props.sourceDetail}>
|
|
1588
|
+
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
|
|
1589
|
+
- {` · ${props.sourceDetail}`}
|
|
1590
|
+
+ {` ${chromeGlyph("·")} ${props.sourceDetail}`}
|
|
1591
|
+
</text>
|
|
1592
|
+
</Show>
|
|
1593
|
+
</box>
|
|
1594
|
+
@@ -141,13 +142,15 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|
1595
|
+
return theme.background.raised.base
|
|
1596
|
+
}
|
|
1597
|
+
const marker = () => {
|
|
1598
|
+
- if (row.kind !== "directory") return "≡ "
|
|
1599
|
+
- return props.expandedNodes && !props.expandedNodes.has(row.id) ? "▸ " : "▾ "
|
|
1600
|
+
+ if (row.kind !== "directory") return `${chromeGlyph("≡")} `
|
|
1601
|
+
+ return props.expandedNodes && !props.expandedNodes.has(row.id)
|
|
1602
|
+
+ ? `${chromeGlyph("▸")} `
|
|
1603
|
+
+ : `${chromeGlyph("▾")} `
|
|
1604
|
+
}
|
|
1605
|
+
// Rails run straight down from each ancestor folder; no end hooks.
|
|
1606
|
+
const indent = createMemo(() => {
|
|
1607
|
+
if (list()) return ""
|
|
1608
|
+
- return "│ ".repeat(Math.max(0, Math.min(row.depth, Math.floor((contentWidth() - 3) / 2))))
|
|
1609
|
+
+ return `${chromeGlyph("│")} `.repeat(Math.max(0, Math.min(row.depth, Math.floor((contentWidth() - 3) / 2))))
|
|
1610
|
+
})
|
|
1611
|
+
const status = () => fileTreeRowStatus(row, props.files, reviewed())
|
|
1612
|
+
const statusColor = () => {
|
|
1613
|
+
@@ -249,7 +252,7 @@ function scrollFileTreeRowIntoView(scroll: ScrollBoxRenderable | undefined, top:
|
|
1614
|
+
|
|
1615
|
+
function fileTreeRowStatus(row: FileTreeRow, files: readonly FileTreeItem[], reviewed: boolean) {
|
|
1616
|
+
if (row.fileIndex === undefined) return ""
|
|
1617
|
+
- if (reviewed) return "✓"
|
|
1618
|
+
+ if (reviewed) return chromeGlyph("✓")
|
|
1619
|
+
const status = files[row.fileIndex]?.status
|
|
1620
|
+
return status === "modified" ? "M" : status === "added" ? "A" : status === "deleted" ? "D" : "?"
|
|
1621
|
+
}
|
|
1622
|
+
@@ -259,7 +262,7 @@ function fileTreeRowStatus(row: FileTreeRow, files: readonly FileTreeItem[], rev
|
|
1623
|
+
function truncateDirectoryChain(name: string, maxWidth: number) {
|
|
1624
|
+
if (stringWidth(name) <= maxWidth) return name
|
|
1625
|
+
const kept: string[] = []
|
|
1626
|
+
- let width = stringWidth("…/")
|
|
1627
|
+
+ let width = stringWidth(chromeEllipsis() + "/")
|
|
1628
|
+
for (const segment of name.split("/").toReversed()) {
|
|
1629
|
+
const next = stringWidth(segment) + (kept.length ? 1 : 0)
|
|
1630
|
+
if (width + next > maxWidth) break
|
|
1631
|
+
@@ -267,5 +270,5 @@ function truncateDirectoryChain(name: string, maxWidth: number) {
|
|
1632
|
+
width += next
|
|
1633
|
+
}
|
|
1634
|
+
if (kept.length === 0) return truncateFilePath(name, maxWidth)
|
|
1635
|
+
- return `…/${kept.join("/")}`
|
|
1636
|
+
+ return `${chromeEllipsis()}/${kept.join("/")}`
|
|
1637
|
+
}
|
|
1638
|
+
diff --git a/packages/tui/src/feature-plugins/system/diff-viewer-image.tsx b/packages/tui/src/feature-plugins/system/diff-viewer-image.tsx
|
|
1639
|
+
index cb7f0801790b0c5c3bd6ac4900bddaa84056df03..7dc0b46ff826b7c522c01c6e90e04eb52f50ce80 100644
|
|
1640
|
+
--- a/packages/tui/src/feature-plugins/system/diff-viewer-image.tsx
|
|
1641
|
+
+++ b/packages/tui/src/feature-plugins/system/diff-viewer-image.tsx
|
|
1642
|
+
@@ -5,6 +5,7 @@ import { createResource, createSignal, Match, onCleanup, Show, Switch } from "so
|
|
1643
|
+
import { DialogImagePreview } from "../../component/dialog-image-preview"
|
|
1644
|
+
import { useTheme } from "../../context/theme"
|
|
1645
|
+
import { useDialog } from "../../ui/dialog"
|
|
1646
|
+
+import { chromeLabel } from "../../ui/glyphs"
|
|
1647
|
+
|
|
1648
|
+
export function isDiffImageFile(file: string) {
|
|
1649
|
+
return /\.(png|jpe?g|webp|gif)$/i.test(file)
|
|
1650
|
+
@@ -37,7 +38,7 @@ export function DiffViewerImage(props: {
|
|
1651
|
+
<text fg={theme.text.feedback.error.base}>Could not load image</text>
|
|
1652
|
+
</Match>
|
|
1653
|
+
<Match when={image.loading}>
|
|
1654
|
+
- <text fg={theme.text.muted}>Loading image…</text>
|
|
1655
|
+
+ <text fg={theme.text.muted}>{chromeLabel("Loading image…")}</text>
|
|
1656
|
+
</Match>
|
|
1657
|
+
<Match when={!image.error && image()} keyed>
|
|
1658
|
+
{(bytes) => {
|
|
1659
|
+
diff --git a/packages/tui/src/feature-plugins/system/diff-viewer.tsx b/packages/tui/src/feature-plugins/system/diff-viewer.tsx
|
|
1660
|
+
index 0e333fffcca30f73aeb2781cf7102236d93125c5..18fa75543bff1ceca79b639eb49849b1cf2eadda 100644
|
|
1661
|
+
--- a/packages/tui/src/feature-plugins/system/diff-viewer.tsx
|
|
1662
|
+
+++ b/packages/tui/src/feature-plugins/system/diff-viewer.tsx
|
|
1663
|
+
@@ -20,6 +20,7 @@ import { DiffViewerImage, isDiffImageFile } from "./diff-viewer-image"
|
|
1664
|
+
import { DIALOG_LIST_INSET, DialogHeader, DialogLayout } from "../../ui/dialog"
|
|
1665
|
+
import { DialogSelect } from "../../ui/dialog-select"
|
|
1666
|
+
import { EmptyBorder } from "../../ui/border"
|
|
1667
|
+
+import { asciiGlyphs, chromeGlyph, chromeLabel } from "../../ui/glyphs"
|
|
1668
|
+
import { SESSION_PANEL_SPLIT_MIN_WIDTH } from "../../ui/layout"
|
|
1669
|
+
import { FilePath } from "../../ui/file-path"
|
|
1670
|
+
import { getScrollAcceleration } from "../../util/scroll"
|
|
1671
|
+
@@ -185,7 +186,7 @@ function DiffViewer(props: {
|
|
1672
|
+
if (mode() === "turn") return diff.error ? "Diff unavailable" : undefined
|
|
1673
|
+
if (mode() === "working") return "vs HEAD"
|
|
1674
|
+
if (diff.error) return "Base or diff unavailable"
|
|
1675
|
+
- if (!result()) return "Resolving diff…"
|
|
1676
|
+
+ if (!result()) return chromeLabel("Resolving diff…")
|
|
1677
|
+
const base = result()?.base
|
|
1678
|
+
if (!base) return "Base not reported"
|
|
1679
|
+
return `vs ${base.name}`
|
|
1680
|
+
@@ -271,7 +272,7 @@ function DiffBaseDialog(props: {
|
|
1681
|
+
<box paddingLeft={DIALOG_LIST_INSET} paddingRight={DIALOG_LIST_INSET}>
|
|
1682
|
+
<text fg={branches.error ? theme.text.feedback.error.base : theme.text.muted}>
|
|
1683
|
+
{branches.loading
|
|
1684
|
+
- ? "Loading branches…"
|
|
1685
|
+
+ ? chromeLabel("Loading branches…")
|
|
1686
|
+
: branches.error
|
|
1687
|
+
? "Could not load branches. Reopen the picker to try again."
|
|
1688
|
+
: "No branches found"}
|
|
1689
|
+
@@ -804,7 +805,7 @@ export function DiffViewerContent(props: {
|
|
1690
|
+
title: "Base",
|
|
1691
|
+
titleView: "Base".padEnd(11),
|
|
1692
|
+
value: "base" as const,
|
|
1693
|
+
- description: props.sourceBase?.name ?? "Choose…",
|
|
1694
|
+
+ description: props.sourceBase?.name ?? chromeLabel("Choose…"),
|
|
1695
|
+
onSelect: props.onChooseBase,
|
|
1696
|
+
},
|
|
1697
|
+
]
|
|
1698
|
+
@@ -892,7 +893,7 @@ export function DiffViewerContent(props: {
|
|
1699
|
+
</text>
|
|
1700
|
+
<Show when={props.sourceDetail}>
|
|
1701
|
+
<text fg={theme.text.muted} selectable={false} flexGrow={1} minWidth={0} wrapMode="none" truncate>
|
|
1702
|
+
- {` · ${props.sourceDetail}`}
|
|
1703
|
+
+ {` ${chromeGlyph("·")} ${props.sourceDetail}`}
|
|
1704
|
+
</text>
|
|
1705
|
+
</Show>
|
|
1706
|
+
</box>
|
|
1707
|
+
@@ -905,7 +906,7 @@ export function DiffViewerContent(props: {
|
|
1708
|
+
<Switch>
|
|
1709
|
+
<Match when={props.loading}>
|
|
1710
|
+
<box flexGrow={1} padding={2} paddingTop={compact() ? 1 : 2}>
|
|
1711
|
+
- <text fg={theme.text.muted}>Loading diff…</text>
|
|
1712
|
+
+ <text fg={theme.text.muted}>{chromeLabel("Loading diff…")}</text>
|
|
1713
|
+
</box>
|
|
1714
|
+
</Match>
|
|
1715
|
+
<Match when={!props.loading && props.error}>
|
|
1716
|
+
@@ -997,7 +998,7 @@ export function DiffViewerContent(props: {
|
|
1717
|
+
return (
|
|
1718
|
+
<box ref={(element: BoxRenderable) => registerPatchNode(entry.fileIndex, element)}>
|
|
1719
|
+
<Show when={index() > 0}>
|
|
1720
|
+
- <CardGap glyph="▄" card={background()} surface={surface()} inset={inset()} />
|
|
1721
|
+
+ <CardGap edge="top" glyph={asciiGlyphs() ? "-" : "▄"} card={background()} surface={surface()} inset={inset()} />
|
|
1722
|
+
</Show>
|
|
1723
|
+
<box backgroundColor={background()} marginLeft={inset()} marginRight={inset()}>
|
|
1724
|
+
<box
|
|
1725
|
+
@@ -1038,7 +1039,7 @@ export function DiffViewerContent(props: {
|
|
1726
|
+
</box>
|
|
1727
|
+
<Show when={reviewed()}>
|
|
1728
|
+
<text fg={theme.text.muted} flexShrink={0}>
|
|
1729
|
+
- ✓
|
|
1730
|
+
+ {chromeGlyph("✓")}
|
|
1731
|
+
</text>
|
|
1732
|
+
</Show>
|
|
1733
|
+
<Show when={!image()} fallback={<text fg={theme.text.muted}>Image</text>}>
|
|
1734
|
+
@@ -1051,7 +1052,7 @@ export function DiffViewerContent(props: {
|
|
1735
|
+
</Show>
|
|
1736
|
+
<Show when={reviewed()}>
|
|
1737
|
+
<box position="absolute" left={-inset()} right={-inset()} bottom={0} height={1}>
|
|
1738
|
+
- <CardGap glyph="▀" card={background()} surface={surface()} inset={inset()} />
|
|
1739
|
+
+ <CardGap edge="bottom" glyph={asciiGlyphs() ? "-" : "▀"} card={background()} surface={surface()} inset={inset()} />
|
|
1740
|
+
</box>
|
|
1741
|
+
</Show>
|
|
1742
|
+
</box>
|
|
1743
|
+
@@ -1138,7 +1139,7 @@ export function DiffViewerContent(props: {
|
|
1744
|
+
border={["bottom"]}
|
|
1745
|
+
borderColor={surface()}
|
|
1746
|
+
backgroundColor={theme.background.base}
|
|
1747
|
+
- customBorderChars={{ ...EmptyBorder, horizontal: "▀" }}
|
|
1748
|
+
+ customBorderChars={{ ...EmptyBorder, horizontal: asciiGlyphs() ? "-" : "▀" }}
|
|
1749
|
+
/>
|
|
1750
|
+
</Show>
|
|
1751
|
+
<box height={1} flexShrink={0} flexDirection="row" paddingLeft={2} paddingRight={1}>
|
|
1752
|
+
@@ -1190,17 +1191,17 @@ export function DiffViewerContent(props: {
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** A half-row of base background between cards: the card's color over its width, the pane surface in the insets. */
|
|
1756
|
+
-function CardGap(props: { glyph: "▄" | "▀"; card: RGBA; surface: RGBA; inset: number }) {
|
|
1757
|
+
+function CardGap(props: { edge: "top" | "bottom"; glyph: string; card: RGBA; surface: RGBA; inset: number }) {
|
|
1758
|
+
const theme = useTheme()
|
|
1759
|
+
const half = (color: RGBA, width?: number) => (
|
|
1760
|
+
<box
|
|
1761
|
+
height={1}
|
|
1762
|
+
flexShrink={0}
|
|
1763
|
+
{...(width === undefined ? { flexGrow: 1 } : { width })}
|
|
1764
|
+
- border={[props.glyph === "▄" ? "top" : "bottom"]}
|
|
1765
|
+
+ border={[props.edge]}
|
|
1766
|
+
borderColor={color}
|
|
1767
|
+
backgroundColor={theme.background.base}
|
|
1768
|
+
- customBorderChars={{ ...EmptyBorder, horizontal: props.glyph }}
|
|
1769
|
+
+ customBorderChars={{ ...EmptyBorder, horizontal: asciiGlyphs() ? "-" : props.glyph }}
|
|
1770
|
+
/>
|
|
1771
|
+
)
|
|
1772
|
+
return (
|
|
1773
|
+
diff --git a/packages/tui/src/feature-plugins/system/stats.tsx b/packages/tui/src/feature-plugins/system/stats.tsx
|
|
1774
|
+
index 950d322422dc6bc14ee66ef8b6483f2d0c57319b..ec6fb8625c6b8db28e3ae00dd7c72224f107688d 100644
|
|
1775
|
+
--- a/packages/tui/src/feature-plugins/system/stats.tsx
|
|
1776
|
+
+++ b/packages/tui/src/feature-plugins/system/stats.tsx
|
|
1777
|
+
@@ -6,6 +6,7 @@ import { useTerminalDimensions } from "@opentui/solid"
|
|
1778
|
+
import { createMemo, createResource, createSignal, For, Show } from "solid-js"
|
|
1779
|
+
import { Logo } from "../../component/logo"
|
|
1780
|
+
import { useTheme } from "../../context/theme"
|
|
1781
|
+
+import { chromeLabel } from "../../ui/glyphs"
|
|
1782
|
+
import { tint } from "../../theme/color"
|
|
1783
|
+
import { statsMetrics, statsNumber } from "./stats-data"
|
|
1784
|
+
|
|
1785
|
+
@@ -170,7 +171,7 @@ function StatsPage(props: { context: Plugin.Context; onClose: () => void }) {
|
|
1786
|
+
<text fg={theme.text.feedback.error.base}>Could not load stats. Reopen /stats to try again.</text>
|
|
1787
|
+
}
|
|
1788
|
+
>
|
|
1789
|
+
- <Show when={result()} fallback={<text fg={theme.text.muted}>Gathering your stats…</text>}>
|
|
1790
|
+
+ <Show when={result()} fallback={<text fg={theme.text.muted}>{chromeLabel("Gathering your stats…")}</text>}>
|
|
1791
|
+
{(value) => <StatsPoster stats={value()} />}
|
|
1792
|
+
</Show>
|
|
1793
|
+
</Show>
|
|
1794
|
+
diff --git a/packages/tui/src/feature-plugins/system/storybook/footer.tsx b/packages/tui/src/feature-plugins/system/storybook/footer.tsx
|
|
1795
|
+
index 8b1fe2a51959f523f35cae10d223bfb55245934f..bead551cb82db977f41271877c51835bb0d524ea 100644
|
|
1796
|
+
--- a/packages/tui/src/feature-plugins/system/storybook/footer.tsx
|
|
1797
|
+
+++ b/packages/tui/src/feature-plugins/system/storybook/footer.tsx
|
|
1798
|
+
@@ -1,5 +1,6 @@
|
|
1799
|
+
import type { Plugin } from "@opencode/plugin/tui"
|
|
1800
|
+
import { For, Show } from "solid-js"
|
|
1801
|
+
+import { chromeGlyph } from "../../../ui/glyphs"
|
|
1802
|
+
|
|
1803
|
+
export type StoryFooterControl = {
|
|
1804
|
+
shortcut: string
|
|
1805
|
+
@@ -21,7 +22,7 @@ export function StoryFooter(props: {
|
|
1806
|
+
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
|
|
1807
|
+
<text fg={theme.text.base}>{props.title}</text>
|
|
1808
|
+
<Show when={props.details?.length}>
|
|
1809
|
+
- <text fg={theme.text.muted}> · {props.details?.join(" · ")}</text>
|
|
1810
|
+
+ <text fg={theme.text.muted}> {chromeGlyph("·")} {props.details?.join(` ${chromeGlyph("·")} `)}</text>
|
|
1811
|
+
</Show>
|
|
1812
|
+
</box>
|
|
1813
|
+
<Show when={props.status || props.message}>
|
|
1814
|
+
@@ -29,7 +30,7 @@ export function StoryFooter(props: {
|
|
1815
|
+
<text fg={theme.text.base} wrapMode="none">
|
|
1816
|
+
{props.status ?? ""}
|
|
1817
|
+
<span style={{ fg: theme.text.muted }}>
|
|
1818
|
+
- {props.status && props.message ? " · " : ""}
|
|
1819
|
+
+ {props.status && props.message ? ` ${chromeGlyph("·")} ` : ""}
|
|
1820
|
+
{props.message ?? ""}
|
|
1821
|
+
</span>
|
|
1822
|
+
</text>
|
|
1823
|
+
diff --git a/packages/tui/src/feature-plugins/system/storybook/index.tsx b/packages/tui/src/feature-plugins/system/storybook/index.tsx
|
|
1824
|
+
index f69e3da68b8b14905443d7e77539d7db013e5599..aab75d8fc8242619d758ee95393af04e0aefdf0d 100644
|
|
1825
|
+
--- a/packages/tui/src/feature-plugins/system/storybook/index.tsx
|
|
1826
|
+
+++ b/packages/tui/src/feature-plugins/system/storybook/index.tsx
|
|
1827
|
+
@@ -8,6 +8,7 @@ import { sessionLocationMissingStory } from "./session-location-missing"
|
|
1828
|
+
import { oneCellSpinnerStory } from "./one-cell-spinner"
|
|
1829
|
+
import { linkClicksStory } from "./link-clicks"
|
|
1830
|
+
import { highlightsStory } from "./highlights"
|
|
1831
|
+
+import { chromeGlyph } from "../../../ui/glyphs"
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* A story is a full-screen, fixture-driven simulation of a real production component. Stories own
|
|
1835
|
+
@@ -115,7 +116,7 @@ function StorybookIndex(props: { context: Plugin.Context }) {
|
|
1836
|
+
<For each={stories}>
|
|
1837
|
+
{(story, index) => (
|
|
1838
|
+
<text fg={index() === selected() ? theme.text.base : theme.text.muted}>
|
|
1839
|
+
- {index() === selected() ? "› " : " "}
|
|
1840
|
+
+ {index() === selected() ? `${chromeGlyph("›")} ` : " "}
|
|
1841
|
+
{index() + 1} {story.title}
|
|
1842
|
+
</text>
|
|
1843
|
+
)}
|
|
1844
|
+
@@ -126,7 +127,7 @@ function StorybookIndex(props: { context: Plugin.Context }) {
|
|
1845
|
+
context={props.context}
|
|
1846
|
+
title="storybook"
|
|
1847
|
+
controls={[
|
|
1848
|
+
- { shortcut: "↑/↓", label: "select" },
|
|
1849
|
+
+ { shortcut: `${chromeGlyph("↑")}/${chromeGlyph("↓")}`, label: "select" },
|
|
1850
|
+
{ shortcut: "enter", label: "open" },
|
|
1851
|
+
{ shortcut: "esc", label: "home" },
|
|
1852
|
+
]}
|
|
1853
|
+
diff --git a/packages/tui/src/feature-plugins/system/storybook/merman-layouts.tsx b/packages/tui/src/feature-plugins/system/storybook/merman-layouts.tsx
|
|
1854
|
+
index 19c6ed906b936220047063ff1598b8d9e8d04717..d7348f8e3b10f387480eb10283fe71bec8b65765 100644
|
|
1855
|
+
--- a/packages/tui/src/feature-plugins/system/storybook/merman-layouts.tsx
|
|
1856
|
+
+++ b/packages/tui/src/feature-plugins/system/storybook/merman-layouts.tsx
|
|
1857
|
+
@@ -5,6 +5,7 @@ import { useTheme, useThemes } from "../../../context/theme"
|
|
1858
|
+
import { usePlugin } from "../../../plugin/context"
|
|
1859
|
+
import type { Story } from "./index"
|
|
1860
|
+
import { StoryFooter } from "./footer"
|
|
1861
|
+
+import { chromeGlyph } from "../../../ui/glyphs"
|
|
1862
|
+
|
|
1863
|
+
const fixtures = [
|
|
1864
|
+
{
|
|
1865
|
+
@@ -185,7 +186,7 @@ function MermanLayoutsStory(props: { context: Plugin.Context }) {
|
|
1866
|
+
title="storybook / Mermaid layouts"
|
|
1867
|
+
details={[`${selected() + 1}/${fixtures.length}`, fixture().id, `${dimensions().width}x${dimensions().height}`]}
|
|
1868
|
+
controls={[
|
|
1869
|
+
- { shortcut: "j/k or ←/→", label: "fixture" },
|
|
1870
|
+
+ { shortcut: `j/k or ${chromeGlyph("←")}/${chromeGlyph("→")}`, label: "fixture" },
|
|
1871
|
+
{ shortcut: "r", label: "reset" },
|
|
1872
|
+
{ shortcut: "esc", label: "back" },
|
|
1873
|
+
]}
|
|
1874
|
+
diff --git a/packages/tui/src/feature-plugins/system/storybook/session-location-missing.tsx b/packages/tui/src/feature-plugins/system/storybook/session-location-missing.tsx
|
|
1875
|
+
index c62d07b95414c2d9cb0b2ecf60e20c939081cf12..fd17fd9a0e5e8c280c441d79f27fe2cd7d9b7e81 100644
|
|
1876
|
+
--- a/packages/tui/src/feature-plugins/system/storybook/session-location-missing.tsx
|
|
1877
|
+
+++ b/packages/tui/src/feature-plugins/system/storybook/session-location-missing.tsx
|
|
1878
|
+
@@ -6,6 +6,7 @@ import { DialogWorkspaces } from "../../../component/dialog-workspaces"
|
|
1879
|
+
import { SessionLocationUnavailable } from "../../../routes/session/location-missing"
|
|
1880
|
+
import type { Story } from "./index"
|
|
1881
|
+
import { StoryFooter } from "./footer"
|
|
1882
|
+
+import { chromeGlyph } from "../../../ui/glyphs"
|
|
1883
|
+
|
|
1884
|
+
const directory = "/Users/kit/code/open-source/opencode-workerd-profile"
|
|
1885
|
+
|
|
1886
|
+
@@ -50,12 +51,12 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
|
1887
|
+
<text fg={theme.text.base} attributes={TextAttributes.BOLD}>
|
|
1888
|
+
Workerd Modal workspace driver
|
|
1889
|
+
</text>
|
|
1890
|
+
- <text fg={theme.text.muted}>build · GPT-5.6 Sol (high)</text>
|
|
1891
|
+
+ <text fg={theme.text.muted}>build {chromeGlyph("·")} GPT-5.6 Sol (high)</text>
|
|
1892
|
+
<box height={1} />
|
|
1893
|
+
<text fg={theme.text.base}>You</text>
|
|
1894
|
+
<text fg={theme.text.muted}>Test the mounted workspace and verify the deployment.</text>
|
|
1895
|
+
<box height={1} />
|
|
1896
|
+
- <text fg={theme.text.base}>Build · GPT-5.6 Sol (high)</text>
|
|
1897
|
+
+ <text fg={theme.text.base}>Build {chromeGlyph("·")} GPT-5.6 Sol (high)</text>
|
|
1898
|
+
<text fg={theme.text.muted}>The deployment is verified and the worktree is clean.</text>
|
|
1899
|
+
<box flexGrow={1} />
|
|
1900
|
+
<SessionLocationUnavailable directory={directory} onMove={open} />
|
|
1901
|
+
diff --git a/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx b/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx
|
|
1902
|
+
index 81a854320c883fdcdf2dcb46c0920cc63e4517bf..69321995d903d39133e782c042ad6b9354e6fe43 100644
|
|
1903
|
+
--- a/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx
|
|
1904
|
+
+++ b/packages/tui/src/feature-plugins/system/storybook/session-tabs.tsx
|
|
1905
|
+
@@ -13,6 +13,7 @@ import {
|
|
1906
|
+
} from "../../../component/session-tabs"
|
|
1907
|
+
import { closeSessionTab, cycleSessionTab, moveSessionTab } from "../../../context/session-tabs-model"
|
|
1908
|
+
import { StoryFooter } from "./footer"
|
|
1909
|
+
+import { chromeGlyph, chromeLabel } from "../../../ui/glyphs"
|
|
1910
|
+
import { DialogPrompt } from "../../../ui/dialog-prompt"
|
|
1911
|
+
import { useDialog } from "../../../ui/dialog"
|
|
1912
|
+
import { DialogSelect } from "../../../ui/dialog-select"
|
|
1913
|
+
@@ -276,26 +277,26 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|
1914
|
+
return lines
|
|
1915
|
+
}
|
|
1916
|
+
lines.push(
|
|
1917
|
+
- { text: "● Taking a look — reading the relevant code first.", color: theme.text.base },
|
|
1918
|
+
+ { text: `${chromeGlyph("●")} Taking a look — reading the relevant code first.`, color: theme.text.base },
|
|
1919
|
+
{ text: "", color: theme.text.base },
|
|
1920
|
+
- { text: ` ✱ Read ${file}`, color: theme.text.muted },
|
|
1921
|
+
- { text: ` ✱ Edit ${file}`, color: theme.text.muted },
|
|
1922
|
+
- { text: ` ✱ Bash bun run test`, color: theme.text.muted },
|
|
1923
|
+
+ { text: ` ${chromeGlyph("✱")} Read ${file}`, color: theme.text.muted },
|
|
1924
|
+
+ { text: ` ${chromeGlyph("✱")} Edit ${file}`, color: theme.text.muted },
|
|
1925
|
+
+ { text: ` ${chromeGlyph("✱")} Bash bun run test`, color: theme.text.muted },
|
|
1926
|
+
{ text: "", color: theme.text.base },
|
|
1927
|
+
)
|
|
1928
|
+
if (status.attention === "question")
|
|
1929
|
+
lines.push({ text: "? Which approach should I take?", color: theme.hue.accent[200] })
|
|
1930
|
+
else if (status.attention === "permission")
|
|
1931
|
+
lines.push({ text: "! Waiting for permission to run the command", color: theme.hue.accent[200] })
|
|
1932
|
+
- else if (status.busy) lines.push({ text: "● Working…", color: theme.hue.interactive[200] })
|
|
1933
|
+
+ else if (status.busy) lines.push({ text: `${chromeGlyph("●")} ${chromeLabel("Working…")}`, color: theme.hue.interactive[200] })
|
|
1934
|
+
else if (outcome === "failed")
|
|
1935
|
+
lines.push({
|
|
1936
|
+
- text: `✗ bun run test failed — 3 tests failing in ${file}`,
|
|
1937
|
+
+ text: `${chromeGlyph("✗")} bun run test failed — 3 tests failing in ${file}`,
|
|
1938
|
+
color: theme.text.feedback.error.base,
|
|
1939
|
+
})
|
|
1940
|
+
else
|
|
1941
|
+
lines.push({
|
|
1942
|
+
- text: `✓ Done — updated ${file} and the tests pass.`,
|
|
1943
|
+
+ text: `${chromeGlyph("✓")} Done — updated ${file} and the tests pass.`,
|
|
1944
|
+
color: theme.text.feedback.success.base,
|
|
1945
|
+
})
|
|
1946
|
+
return lines
|
|
1947
|
+
@@ -307,7 +308,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|
1948
|
+
const waiting = values.filter((status) => status.attention).length
|
|
1949
|
+
const unread = values.filter((status) => status.unread !== undefined).length
|
|
1950
|
+
return [`selected ${number(active() ?? "")}`, `${running} running`, `${waiting} waiting`, `${unread} unread`].join(
|
|
1951
|
+
- " · ",
|
|
1952
|
+
+ ` ${chromeGlyph("·")} `,
|
|
1953
|
+
)
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
@@ -529,7 +530,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|
1957
|
+
context={props.context}
|
|
1958
|
+
title="storybook / tabs"
|
|
1959
|
+
details={[
|
|
1960
|
+
- vertical() ? `${railCompact() ? "compact rail" : "expanded rail"} · ${resize.size()} cols` : "top strip",
|
|
1961
|
+
+ vertical() ? `${railCompact() ? "compact rail" : "expanded rail"} ${chromeGlyph("·")} ${resize.size()} cols` : "top strip",
|
|
1962
|
+
spinner(),
|
|
1963
|
+
indicators(),
|
|
1964
|
+
`${TAB_UNREAD_MARKERS[marker()]} ${marker()}`,
|
|
1965
|
+
@@ -552,7 +553,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|
1966
|
+
{ shortcut: "g", label: "status/numbers" },
|
|
1967
|
+
{ shortcut: "u", label: "unread marker" },
|
|
1968
|
+
{ shortcut: "m", label: "motion" },
|
|
1969
|
+
- { shortcut: "↑/↓", label: "select" },
|
|
1970
|
+
+ { shortcut: `${chromeGlyph("↑")}/${chromeGlyph("↓")}`, label: "select" },
|
|
1971
|
+
{ shortcut: "o", label: "layout" },
|
|
1972
|
+
{ shortcut: "drag edge", label: "resize / double-click reset" },
|
|
1973
|
+
{ shortcut: "r", label: "reset idle" },
|
|
1974
|
+
diff --git a/packages/tui/src/routes/session/activity-summary.ts b/packages/tui/src/routes/session/activity-summary.ts
|
|
1975
|
+
index 6578f50d632ae57f4c15069dac23131664d95691..a9676c9031424d80969ee45d0b5b09a812e487a1 100644
|
|
1976
|
+
--- a/packages/tui/src/routes/session/activity-summary.ts
|
|
1977
|
+
+++ b/packages/tui/src/routes/session/activity-summary.ts
|
|
1978
|
+
@@ -9,6 +9,7 @@ import { visitEntries } from "./anchor-view"
|
|
1979
|
+
import { instructionPaths, type PartRef, type SessionEntry, type SessionNode } from "./grouping/session"
|
|
1980
|
+
import { reasoningContent } from "./message-parts"
|
|
1981
|
+
import { resolvePart } from "./rows"
|
|
1982
|
+
+import { chromeEllipsis, chromeLabel } from "../../ui/glyphs"
|
|
1983
|
+
|
|
1984
|
+
type Item = {
|
|
1985
|
+
message: SessionMessageAssistant
|
|
1986
|
+
@@ -45,10 +46,12 @@ export function summarizeActivity(
|
|
1987
|
+
|
|
1988
|
+
/** Status for a running item, independent of its details, so it doesn't flicker as they arrive. */
|
|
1989
|
+
export function busyLabel(part: Item["part"]) {
|
|
1990
|
+
- if (part.type === "reasoning") return "Thinking…"
|
|
1991
|
+
+ if (part.type === "reasoning") return chromeLabel("Thinking…")
|
|
1992
|
+
const name = canonicalToolName(part.name)
|
|
1993
|
+
const noun = name === "shell" ? "command" : name === "execute" ? "code" : name
|
|
1994
|
+
- return `${part.state.status === "streaming" ? "Preparing" : "Running"} ${noun}…`
|
|
1995
|
+
+ const verb = part.state.status === "streaming" ? "Preparing" : "Running"
|
|
1996
|
+
+ if (noun === "command" || noun === "code") return `${verb} ${noun}${chromeEllipsis()}`
|
|
1997
|
+
+ return `${verb} ${noun}${chromeEllipsis()}`
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
diff --git a/packages/tui/src/routes/session/composer/index.tsx b/packages/tui/src/routes/session/composer/index.tsx
|
|
2002
|
+
index 2af8bfb521a6327967c1c57c1fdb13842518fa71..4427c90c996b0ce00b2120707c52202f71eb02db 100644
|
|
2003
|
+
--- a/packages/tui/src/routes/session/composer/index.tsx
|
|
2004
|
+
+++ b/packages/tui/src/routes/session/composer/index.tsx
|
|
2005
|
+
@@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"
|
|
2006
|
+
import { TextAttributes } from "@opentui/core"
|
|
2007
|
+
import { useTheme } from "../../../context/theme"
|
|
2008
|
+
import { SplitBorder } from "../../../ui/border"
|
|
2009
|
+
+import { chromeGlyph } from "../../../ui/glyphs"
|
|
2010
|
+
import { Keymap } from "../../../context/keymap"
|
|
2011
|
+
import { SubagentsTab } from "./subagents-tab"
|
|
2012
|
+
import { ShellTab } from "./shell-tab"
|
|
2013
|
+
@@ -145,7 +146,7 @@ export function Composer(props: ComposerProps) {
|
|
2014
|
+
<span style={{ fg: theme.text.base }}>
|
|
2015
|
+
<b>tabs</b>{" "}
|
|
2016
|
+
</span>
|
|
2017
|
+
- <span style={{ fg: theme.text.muted }}>←/→</span>
|
|
2018
|
+
+ <span style={{ fg: theme.text.muted }}>{`${chromeGlyph("←")}/${chromeGlyph("→")}`}</span>
|
|
2019
|
+
</text>
|
|
2020
|
+
</Show>
|
|
2021
|
+
</box>
|
|
2022
|
+
diff --git a/packages/tui/src/routes/session/dialog-execute.tsx b/packages/tui/src/routes/session/dialog-execute.tsx
|
|
2023
|
+
index 86f9c8f1c52161f1135debb74cc2dc96e81c23e8..b8d17400714805aa8c9607a34fe6582e379242a5 100644
|
|
2024
|
+
--- a/packages/tui/src/routes/session/dialog-execute.tsx
|
|
2025
|
+
+++ b/packages/tui/src/routes/session/dialog-execute.tsx
|
|
2026
|
+
@@ -17,6 +17,7 @@ import {
|
|
2027
|
+
dialogWidth,
|
|
2028
|
+
useDialog,
|
|
2029
|
+
} from "../../ui/dialog"
|
|
2030
|
+
+import { chromeGlyph, chromeLabel } from "../../ui/glyphs"
|
|
2031
|
+
import { useToast } from "../../ui/toast"
|
|
2032
|
+
import { Locale } from "../../util/locale"
|
|
2033
|
+
import { getScrollAcceleration } from "../../util/scroll"
|
|
2034
|
+
@@ -67,10 +68,10 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
|
2035
|
+
})
|
|
2036
|
+
const status = createMemo(() => {
|
|
2037
|
+
const state = props.part.state
|
|
2038
|
+
- if (state.status === "streaming") return "Receiving code…"
|
|
2039
|
+
+ if (state.status === "streaming") return chromeLabel("Receiving code…")
|
|
2040
|
+
if (state.status === "running") return "Running"
|
|
2041
|
+
const duration = props.part.time.completed
|
|
2042
|
+
- ? ` · ${Locale.duration(props.part.time.completed - (props.part.time.ran ?? props.part.time.created))}`
|
|
2043
|
+
+ ? ` ${chromeGlyph("·")} ${Locale.duration(props.part.time.completed - (props.part.time.ran ?? props.part.time.created))}`
|
|
2044
|
+
: ""
|
|
2045
|
+
if (failed()) return `Failed${duration}`
|
|
2046
|
+
return `Completed${duration}`
|
|
2047
|
+
@@ -101,10 +102,10 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
|
2048
|
+
|
|
2049
|
+
const copyActions = () => [
|
|
2050
|
+
copied() === "code"
|
|
2051
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: () => copy("code") }
|
|
2052
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: () => copy("code") }
|
|
2053
|
+
: { title: "copy code", key: "c", onTrigger: () => copy("code") },
|
|
2054
|
+
copied() === "output"
|
|
2055
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: () => copy("output") }
|
|
2056
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: () => copy("output") }
|
|
2057
|
+
: { title: "copy output", key: "o", onTrigger: () => copy("output") },
|
|
2058
|
+
]
|
|
2059
|
+
|
|
2060
|
+
@@ -127,7 +128,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
|
2061
|
+
<DialogFooter
|
|
2062
|
+
actions={{
|
|
2063
|
+
secondary: [
|
|
2064
|
+
- { title: "scroll", key: "↑/↓" },
|
|
2065
|
+
+ { title: "scroll", key: `${chromeGlyph("↑")}/${chromeGlyph("↓")}` },
|
|
2066
|
+
...copyActions(),
|
|
2067
|
+
{ title: "back", key: "esc", onTrigger: () => dialog.clear() },
|
|
2068
|
+
],
|
|
2069
|
+
@@ -150,7 +151,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
|
2070
|
+
<text fg={contentTheme.text.muted} attributes={TextAttributes.BOLD}>
|
|
2071
|
+
Code
|
|
2072
|
+
</text>
|
|
2073
|
+
- <Show when={code()} fallback={<text fg={contentTheme.text.muted}>Waiting for code…</text>}>
|
|
2074
|
+
+ <Show when={code()} fallback={<text fg={contentTheme.text.muted}>{chromeLabel("Waiting for code…")}</text>}>
|
|
2075
|
+
{(value) => <GutteredCode content={value()} filetype="typescript" digits={digits()} />}
|
|
2076
|
+
</Show>
|
|
2077
|
+
</box>
|
|
2078
|
+
@@ -171,7 +172,7 @@ export function DialogExecute(props: { part: SessionMessageAssistantTool }) {
|
|
2079
|
+
}
|
|
2080
|
+
wrapMode="word"
|
|
2081
|
+
>
|
|
2082
|
+
- {text() ?? (props.part.state.status === "completed" ? "No output" : "Waiting for output…")}
|
|
2083
|
+
+ {text() ?? (props.part.state.status === "completed" ? "No output" : chromeLabel("Waiting for output…"))}
|
|
2084
|
+
</text>
|
|
2085
|
+
}
|
|
2086
|
+
>
|
|
2087
|
+
diff --git a/packages/tui/src/routes/session/dialog-fork.tsx b/packages/tui/src/routes/session/dialog-fork.tsx
|
|
2088
|
+
index 2ba1a27fe144989498a33a5f113d4a9e41c0f25f..df2666114f13caa68e5c34a9b3c891d94b277fb7 100644
|
|
2089
|
+
--- a/packages/tui/src/routes/session/dialog-fork.tsx
|
|
2090
|
+
+++ b/packages/tui/src/routes/session/dialog-fork.tsx
|
|
2091
|
+
@@ -3,6 +3,7 @@ import { useData } from "../../context/data"
|
|
2092
|
+
import { useRoute } from "../../context/route"
|
|
2093
|
+
import { useClient } from "../../context/client"
|
|
2094
|
+
import { Spinner } from "../../component/spinner"
|
|
2095
|
+
+import { chromeLabel } from "../../ui/glyphs"
|
|
2096
|
+
import { DialogSelect, type DialogSelectOption } from "../../ui/dialog-select"
|
|
2097
|
+
import { DIALOG_LIST_INSET, DialogHeader, DialogLayout, useDialog } from "../../ui/dialog"
|
|
2098
|
+
import { useToast } from "../../ui/toast"
|
|
2099
|
+
@@ -75,7 +76,7 @@ export function DialogFork(props: { sessionID: string; messageID?: string; onMov
|
|
2100
|
+
when={!pending()}
|
|
2101
|
+
fallback={
|
|
2102
|
+
<DialogLayout inset={DIALOG_LIST_INSET} header={<DialogHeader title="Fork session" />}>
|
|
2103
|
+
- <Spinner>Forking session…</Spinner>
|
|
2104
|
+
+ <Spinner>{chromeLabel("Forking session…")}</Spinner>
|
|
2105
|
+
</DialogLayout>
|
|
2106
|
+
}
|
|
2107
|
+
>
|
|
2108
|
+
diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx
|
|
2109
|
+
index cba007198f026230d5c2051379fa7a9056c6aa52..220b491c7eb10a6407f127f6b9afae4e14a01716 100644
|
|
2110
|
+
--- a/packages/tui/src/routes/session/form.tsx
|
|
2111
|
+
+++ b/packages/tui/src/routes/session/form.tsx
|
|
2112
|
+
@@ -15,6 +15,7 @@ import type { FormAnswer, FormField, FormValue } from "@opencode/client"
|
|
2113
|
+
import { useData, type FormWithLocation } from "../../context/data"
|
|
2114
|
+
import { useClipboard } from "../../context/clipboard"
|
|
2115
|
+
import { SplitBorder } from "../../ui/border"
|
|
2116
|
+
+import { chromeGlyph, chromeEllipsis } from "../../ui/glyphs"
|
|
2117
|
+
import { useToast } from "../../ui/toast"
|
|
2118
|
+
import { Keymap } from "../../context/keymap"
|
|
2119
|
+
import { useInteractivity } from "../../context/interactivity"
|
|
2120
|
+
@@ -38,7 +39,7 @@ import type { FormAnswerField } from "../../util/form"
|
|
2121
|
+
export const FORM_MODE = "form"
|
|
2122
|
+
|
|
2123
|
+
function truncate(label: string, max: number) {
|
|
2124
|
+
- return label.length > max ? label.slice(0, max - 1).trimEnd() + "…" : label
|
|
2125
|
+
+ return label.length > max ? label.slice(0, max - 1).trimEnd() + chromeEllipsis() : label
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
type FormDraft = {
|
|
2129
|
+
@@ -787,7 +788,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2130
|
+
</text>
|
|
2131
|
+
<Show when={fields().length > 0}>
|
|
2132
|
+
<text fg={theme.text.muted}>
|
|
2133
|
+
- · {answered()}/{fields().length} completed
|
|
2134
|
+
+ {chromeGlyph("·")} {answered()}/{fields().length} completed
|
|
2135
|
+
</text>
|
|
2136
|
+
</Show>
|
|
2137
|
+
</box>
|
|
2138
|
+
@@ -868,7 +869,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2139
|
+
fg={store.answers[external().key] === true ? theme.text.feedback.success.base : theme.text.muted}
|
|
2140
|
+
>
|
|
2141
|
+
{store.answers[external().key] === true
|
|
2142
|
+
- ? "✓ Acknowledged"
|
|
2143
|
+
+ ? `${chromeGlyph("✓")} Acknowledged`
|
|
2144
|
+
: store.externalReady[external().key]
|
|
2145
|
+
? "Complete the external action, then press enter to confirm."
|
|
2146
|
+
: "Open or copy the URL, complete the external action, then confirm."}
|
|
2147
|
+
@@ -952,7 +953,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2148
|
+
: theme.text.muted
|
|
2149
|
+
}
|
|
2150
|
+
>
|
|
2151
|
+
- [{picked() ? "✓" : " "}]
|
|
2152
|
+
+ [{picked() ? chromeGlyph("✓") : " "}]
|
|
2153
|
+
</text>
|
|
2154
|
+
</Show>
|
|
2155
|
+
<text fg={active() ? theme.text.formfield.focused : theme.text.formfield.base}>
|
|
2156
|
+
@@ -960,7 +961,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2157
|
+
</text>
|
|
2158
|
+
</box>
|
|
2159
|
+
<Show when={!multi()}>
|
|
2160
|
+
- <text fg={theme.text.formfield.selected}>{picked() ? " ✓" : ""}</text>
|
|
2161
|
+
+ <text fg={theme.text.formfield.selected}>{picked() ? ` ${chromeGlyph("✓")}` : ""}</text>
|
|
2162
|
+
</Show>
|
|
2163
|
+
</box>
|
|
2164
|
+
<Show when={row.description}>
|
|
2165
|
+
@@ -1007,7 +1008,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2166
|
+
: theme.text.muted
|
|
2167
|
+
}
|
|
2168
|
+
>
|
|
2169
|
+
- [{customChecked() ? "✓" : " "}]
|
|
2170
|
+
+ [{customChecked() ? chromeGlyph("✓") : " "}]
|
|
2171
|
+
</text>
|
|
2172
|
+
</Show>
|
|
2173
|
+
<Show
|
|
2174
|
+
@@ -1018,7 +1019,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2175
|
+
{input() || "Type your own answer"}
|
|
2176
|
+
</text>
|
|
2177
|
+
<Show when={!multi() && customPicked()}>
|
|
2178
|
+
- <text fg={theme.text.formfield.selected}>✓</text>
|
|
2179
|
+
+ <text fg={theme.text.formfield.selected}>{chromeGlyph("✓")}</text>
|
|
2180
|
+
</Show>
|
|
2181
|
+
</>
|
|
2182
|
+
}
|
|
2183
|
+
@@ -1134,17 +1135,17 @@ export function FormPrompt(props: { form: FormWithLocation }) {
|
|
2184
|
+
<box flexDirection="row" gap={2}>
|
|
2185
|
+
<Show when={!single()}>
|
|
2186
|
+
<text fg={theme.text.base}>
|
|
2187
|
+
- {"⇆"} <span style={{ fg: theme.text.muted }}>tab</span>
|
|
2188
|
+
+ {chromeGlyph("⇆")} <span style={{ fg: theme.text.muted }}>tab</span>
|
|
2189
|
+
</text>
|
|
2190
|
+
</Show>
|
|
2191
|
+
<Show when={!confirm() && !textual() && !externalField() && !store.editing}>
|
|
2192
|
+
<text fg={theme.text.base}>
|
|
2193
|
+
- {"↑↓"} <span style={{ fg: theme.text.muted }}>select</span>
|
|
2194
|
+
+ {`${chromeGlyph("↑")}${chromeGlyph("↓")}`} <span style={{ fg: theme.text.muted }}>select</span>
|
|
2195
|
+
</text>
|
|
2196
|
+
</Show>
|
|
2197
|
+
<Show when={confirm() && reviewContentHeight() > reviewMaxHeight()}>
|
|
2198
|
+
<text fg={theme.text.base}>
|
|
2199
|
+
- {"↑↓"} <span style={{ fg: theme.text.muted }}>scroll</span>
|
|
2200
|
+
+ {`${chromeGlyph("↑")}${chromeGlyph("↓")}`} <span style={{ fg: theme.text.muted }}>scroll</span>
|
|
2201
|
+
</text>
|
|
2202
|
+
</Show>
|
|
2203
|
+
<text
|
|
2204
|
+
diff --git a/packages/tui/src/routes/session/group-view.tsx b/packages/tui/src/routes/session/group-view.tsx
|
|
2205
|
+
index 2e09fc5e84974454fdc4b2106ab89f68c99f13da..bcf83e0c9cbd66ac9e51791c4e880ca6e2b0f791 100644
|
|
2206
|
+
--- a/packages/tui/src/routes/session/group-view.tsx
|
|
2207
|
+
+++ b/packages/tui/src/routes/session/group-view.tsx
|
|
2208
|
+
@@ -5,6 +5,7 @@ import type { SessionMessageAssistantTool, SessionMessageInfo } from "@opencode/
|
|
2209
|
+
import { createSyntaxStyleMemo, useTheme, useThemes } from "../../context/theme"
|
|
2210
|
+
import { reasoningSummary } from "../../context/thinking"
|
|
2211
|
+
import { SplitBorder } from "../../ui/border"
|
|
2212
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
2213
|
+
import { Locale } from "../../util/locale"
|
|
2214
|
+
import { EntryAnchor, GroupAnchor, visitEntries } from "./anchor-view"
|
|
2215
|
+
import { groupID } from "./anchors"
|
|
2216
|
+
@@ -142,7 +143,7 @@ function GroupContent(props: GroupProps) {
|
|
2217
|
+
<Show when={grouped()} fallback={children("normal")}>
|
|
2218
|
+
<Show when={tools().length > 0}>
|
|
2219
|
+
<InlineToolRow
|
|
2220
|
+
- icon={completed() ? "→" : "✱"}
|
|
2221
|
+
+ icon={chromeGlyph(completed() ? "→" : "✱")}
|
|
2222
|
+
color={hover() ? theme.text.base : theme.text.muted}
|
|
2223
|
+
complete={completed()}
|
|
2224
|
+
pending={label()}
|
|
2225
|
+
@@ -184,8 +185,8 @@ function GroupContent(props: GroupProps) {
|
|
2226
|
+
>
|
|
2227
|
+
{props.completed ? "Thought" : latest() ? `Thinking: ${latest()}` : "Thinking"}
|
|
2228
|
+
<Show when={props.completed && !expanded() && latest()}>: {latest()}</Show>
|
|
2229
|
+
- <Show when={props.completed && thoughts().length > 1}> · {thoughts().length} steps</Show>
|
|
2230
|
+
- <Show when={props.completed && duration()}> · {Locale.duration(duration())}</Show>
|
|
2231
|
+
+ <Show when={props.completed && thoughts().length > 1}> {chromeGlyph("·")} {thoughts().length} steps</Show>
|
|
2232
|
+
+ <Show when={props.completed && duration()}> {chromeGlyph("·")} {Locale.duration(duration())}</Show>
|
|
2233
|
+
</InlineToolRow>
|
|
2234
|
+
<Show when={expanded()}>
|
|
2235
|
+
<box paddingLeft={3}>{children("thought")}</box>
|
|
2236
|
+
@@ -209,7 +210,7 @@ function ActivityGroup(props: GroupProps) {
|
|
2237
|
+
<GroupAnchor groupID={disclosure.id()} active={summary().label !== ""}>
|
|
2238
|
+
<Show when={summary().label}>
|
|
2239
|
+
<InlineToolRow
|
|
2240
|
+
- icon={disclosure.expanded() ? "−" : "+"}
|
|
2241
|
+
+ icon={activityGroupDisclosureIcon(disclosure.expanded())}
|
|
2242
|
+
color={hover() ? theme.text.base : theme.text.muted}
|
|
2243
|
+
complete={true}
|
|
2244
|
+
pending={summary().label}
|
|
2245
|
+
@@ -247,7 +248,7 @@ function InstructionsGroup(props: GroupProps) {
|
|
2246
|
+
return (
|
|
2247
|
+
<GroupAnchor groupID={disclosure.id()} active={files() > 0}>
|
|
2248
|
+
<InlineToolRow
|
|
2249
|
+
- icon="◈"
|
|
2250
|
+
+ icon={chromeGlyph("◈")}
|
|
2251
|
+
color={hover() ? theme.text.base : theme.text.muted}
|
|
2252
|
+
complete={true}
|
|
2253
|
+
pending=""
|
|
2254
|
+
@@ -290,6 +291,10 @@ function descendants(node: Extract<SessionNode, { type: "group" }>) {
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/** Permission-blocked tools stay visible below the root group, even when collapsed. */
|
|
2258
|
+
+export function activityGroupDisclosureIcon(expanded: boolean) {
|
|
2259
|
+
+ return expanded ? chromeGlyph("−") : "+"
|
|
2260
|
+
+}
|
|
2261
|
+
+
|
|
2262
|
+
function PendingEntries(props: GroupProps & { entries: readonly SessionEntry[] }) {
|
|
2263
|
+
return (
|
|
2264
|
+
<Show when={!props.pendingOutside}>
|
|
2265
|
+
diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx
|
|
2266
|
+
index 0b8228ef8714b05a4e675b6f9e58a116fb9367ba..e7077d07a8897c79659c7ba61d39113616886017 100644
|
|
2267
|
+
--- a/packages/tui/src/routes/session/index.tsx
|
|
2268
|
+
+++ b/packages/tui/src/routes/session/index.tsx
|
|
2269
|
+
@@ -19,6 +19,7 @@ import { useRoute, useRouteData } from "../../context/route"
|
|
2270
|
+
import { createStore } from "solid-js/store"
|
|
2271
|
+
import { useData } from "../../context/data"
|
|
2272
|
+
import { SplitBorder } from "../../ui/border"
|
|
2273
|
+
+import { chromeEllipsis, chromeGlyph, chromeLabel } from "../../ui/glyphs"
|
|
2274
|
+
import { useTuiTerminalEnvironment } from "../../context/runtime"
|
|
2275
|
+
import { Spinner, SPINNER_FRAMES } from "../../component/spinner"
|
|
2276
|
+
import { PatchDiff } from "../../component/patch-diff"
|
|
2277
|
+
@@ -191,7 +192,7 @@ export function Session(props: {
|
|
2278
|
+
createEffect(() => currentLocation.set(location()))
|
|
2279
|
+
|
|
2280
|
+
createEffect(() => {
|
|
2281
|
+
- const title = Locale.truncate(session()?.title ?? "", 50)
|
|
2282
|
+
+ const title = Locale.truncate(session()?.title ?? "", 50, chromeEllipsis())
|
|
2283
|
+
setEpilogue(sessionEpilogue({ title, sessionID: session()?.id }))
|
|
2284
|
+
})
|
|
2285
|
+
onCleanup(() => setEpilogue())
|
|
2286
|
+
@@ -1441,7 +1442,7 @@ export function Session(props: {
|
|
2287
|
+
</box>
|
|
2288
|
+
<box height={1} flexShrink={0} flexDirection="row" justifyContent="flex-end">
|
|
2289
|
+
<Show when={firstJump()}>
|
|
2290
|
+
- <text fg={theme.text.feedback.info.base}>Loading session history…</text>
|
|
2291
|
+
+ <text fg={theme.text.feedback.info.base}>{chromeLabel("Loading session history…")}</text>
|
|
2292
|
+
</Show>
|
|
2293
|
+
<Show when={!firstJump() && awayFromBottom()}>
|
|
2294
|
+
<box
|
|
2295
|
+
@@ -1454,7 +1455,7 @@ export function Session(props: {
|
|
2296
|
+
<text
|
|
2297
|
+
fg={latestHovered() ? theme.text.action.secondary.hovered : theme.text.action.secondary.base}
|
|
2298
|
+
>
|
|
2299
|
+
- Jump to latest ↓
|
|
2300
|
+
+ Jump to latest {chromeGlyph("↓")}
|
|
2301
|
+
</text>
|
|
2302
|
+
</box>
|
|
2303
|
+
</Show>
|
|
2304
|
+
@@ -1677,14 +1678,14 @@ function TurnTokenUsage(props: {
|
|
2305
|
+
<span>{expanded() ? "- " : "+ "}</span>
|
|
2306
|
+
<span style={{ attributes: TextAttributes.BOLD }}>Tokens</span>
|
|
2307
|
+
<span>
|
|
2308
|
+
- : {summary().count} {summary().count === 1 ? "step" : "steps"} · latest:{" "}
|
|
2309
|
+
- {summary().latestNewTokens.toLocaleString()} new · {summary().latestCached.toLocaleString()} cached ·{" "}
|
|
2310
|
+
+ : {summary().count} {summary().count === 1 ? "step" : "steps"} {chromeGlyph("·")} latest:{" "}
|
|
2311
|
+
+ {summary().latestNewTokens.toLocaleString()} new {chromeGlyph("·")} {summary().latestCached.toLocaleString()} cached {chromeGlyph("·")}{" "}
|
|
2312
|
+
{summary().latestTotal.toLocaleString()} total
|
|
2313
|
+
</span>
|
|
2314
|
+
<Show when={summary().reuseDrops > 0}>
|
|
2315
|
+
<span style={{ fg: theme.text.feedback.warning.base }}>
|
|
2316
|
+
{" "}
|
|
2317
|
+
- · ! {summary().reuseDrops} likely cache {summary().reuseDrops === 1 ? "bust" : "busts"}
|
|
2318
|
+
+ {chromeGlyph("·")} ! {summary().reuseDrops} likely cache {summary().reuseDrops === 1 ? "bust" : "busts"}
|
|
2319
|
+
</span>
|
|
2320
|
+
</Show>
|
|
2321
|
+
</text>
|
|
2322
|
+
@@ -1914,16 +1915,16 @@ function AssistantFooter(props: { message: SessionMessageAssistant }) {
|
|
2323
|
+
{Locale.titlecase(props.message.agent)}
|
|
2324
|
+
</span>
|
|
2325
|
+
<Show when={ctx.terminal.width >= 28}>
|
|
2326
|
+
- <span style={{ fg: theme.text.muted }}> · {model()}</span>
|
|
2327
|
+
+ <span style={{ fg: theme.text.muted }}> {chromeGlyph("·")} {model()}</span>
|
|
2328
|
+
</Show>
|
|
2329
|
+
<Show when={duration() && (ctx.terminal.width < 28 || ctx.terminal.width >= 36)}>
|
|
2330
|
+
- <span style={{ fg: theme.text.muted }}> · {Locale.duration(duration())}</span>
|
|
2331
|
+
+ <span style={{ fg: theme.text.muted }}> {chromeGlyph("·")} {Locale.duration(duration())}</span>
|
|
2332
|
+
</Show>
|
|
2333
|
+
<Show when={config.data.session.tps && tokensPerSecond()}>
|
|
2334
|
+
- {(value) => <span style={{ fg: theme.text.muted }}> · {value().toFixed(1)} tok/s</span>}
|
|
2335
|
+
+ {(value) => <span style={{ fg: theme.text.muted }}> {chromeGlyph("·")} {value().toFixed(1)} tok/s</span>}
|
|
2336
|
+
</Show>
|
|
2337
|
+
<Show when={interrupted()}>
|
|
2338
|
+
- <span style={{ fg: theme.text.muted }}> · interrupted</span>
|
|
2339
|
+
+ <span style={{ fg: theme.text.muted }}> {chromeGlyph("·")} interrupted</span>
|
|
2340
|
+
</Show>
|
|
2341
|
+
</text>
|
|
2342
|
+
</box>
|
|
2343
|
+
@@ -1938,7 +1939,7 @@ function SessionSwitchMessageV2(props: { message: SessionMessageInfo }) {
|
|
2344
|
+
return (
|
|
2345
|
+
<box paddingLeft={3}>
|
|
2346
|
+
<text>
|
|
2347
|
+
- <span style={{ fg: theme.text.muted }}>↳ Moved to </span>
|
|
2348
|
+
+ <span style={{ fg: theme.text.muted }}>{chromeGlyph("↳")} Moved to </span>
|
|
2349
|
+
<span style={{ fg: theme.text.feedback.info.base }}>{props.message.location.directory}</span>
|
|
2350
|
+
</text>
|
|
2351
|
+
</box>
|
|
2352
|
+
@@ -1984,8 +1985,9 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
|
2353
|
+
if (state() === "error") return "failed"
|
|
2354
|
+
return state() ?? "finished"
|
|
2355
|
+
}
|
|
2356
|
+
- const heading = () => `${state() === "completed" ? "↳" : "!"} ${actor()} ${status()}`
|
|
2357
|
+
- const suffix = () => Locale.truncateWidth(` · ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())))
|
|
2358
|
+
+ const heading = () => `${chromeGlyph(state() === "completed" ? "↳" : "!")} ${actor()} ${status()}`
|
|
2359
|
+
+ const suffix = () =>
|
|
2360
|
+
+ Locale.truncateWidth(` ${chromeGlyph("·")} ${description()}`, Math.max(0, ctx.width - 3 - stringWidth(heading())), chromeEllipsis())
|
|
2361
|
+
const color = () => {
|
|
2362
|
+
if (state() === "error") return theme.text.feedback.error.base
|
|
2363
|
+
if (state() === "cancelled") return theme.text.feedback.warning.base
|
|
2364
|
+
@@ -1996,7 +1998,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
|
2365
|
+
<Show
|
|
2366
|
+
when={completion()}
|
|
2367
|
+
fallback={
|
|
2368
|
+
- <InlineToolRow icon="◈" color={theme.text.muted} pending="Notice" complete={true}>
|
|
2369
|
+
+ <InlineToolRow icon={chromeGlyph("◈")} color={theme.text.muted} pending="Notice" complete={true}>
|
|
2370
|
+
{text()}
|
|
2371
|
+
</InlineToolRow>
|
|
2372
|
+
}
|
|
2373
|
+
@@ -2023,7 +2025,7 @@ function SessionNoticeMessageV2(props: { message: SessionMessageInfo }) {
|
|
2374
|
+
function SessionSkillMessage(props: { message: Extract<SessionMessageInfo, { type: "skill" }> }) {
|
|
2375
|
+
const theme = useTheme()
|
|
2376
|
+
return (
|
|
2377
|
+
- <InlineToolRow icon="→" color={theme.text.muted} pending="Skill" complete={true}>
|
|
2378
|
+
+ <InlineToolRow icon={chromeGlyph("→")} color={theme.text.muted} pending="Skill" complete={true}>
|
|
2379
|
+
Skill {props.message.name}
|
|
2380
|
+
</InlineToolRow>
|
|
2381
|
+
)
|
|
2382
|
+
@@ -2047,7 +2049,7 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
|
2383
|
+
const input = tokens.input + tokens.cache.read + tokens.cache.write
|
|
2384
|
+
const output = tokens.output + tokens.reasoning
|
|
2385
|
+
if (input + output <= 0) return
|
|
2386
|
+
- return `${Locale.number(input)} in · ${Locale.number(output)} out`
|
|
2387
|
+
+ return `${Locale.number(input)} in ${chromeGlyph("·")} ${Locale.number(output)} out`
|
|
2388
|
+
}
|
|
2389
|
+
return (
|
|
2390
|
+
<box>
|
|
2391
|
+
@@ -2056,12 +2058,12 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
|
2392
|
+
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
|
2393
|
+
<Switch>
|
|
2394
|
+
<Match when={status() === "running"}>
|
|
2395
|
+
- <Show when={ctx.config.animations ?? true} fallback={<text fg={color()}>⋯</text>}>
|
|
2396
|
+
+ <Show when={ctx.config.animations ?? true} fallback={<text fg={color()}>{chromeGlyph("…")}</text>}>
|
|
2397
|
+
<spinner frames={SPINNER_FRAMES} interval={80} color={color()} />
|
|
2398
|
+
</Show>
|
|
2399
|
+
</Match>
|
|
2400
|
+
<Match when={status() === "failed" && !cancelled()}>
|
|
2401
|
+
- <text fg={color()}>✗</text>
|
|
2402
|
+
+ <text fg={color()}>{chromeGlyph("✗")}</text>
|
|
2403
|
+
</Match>
|
|
2404
|
+
</Switch>
|
|
2405
|
+
<text fg={color()}>
|
|
2406
|
+
@@ -2070,10 +2072,10 @@ function CompactionMessage(props: { message: Extract<SessionMessageInfo, { type:
|
|
2407
|
+
: "Compaction"}
|
|
2408
|
+
</text>
|
|
2409
|
+
<Show when={cancelled()}>
|
|
2410
|
+
- <text fg={color()}>· cancelled</text>
|
|
2411
|
+
+ <text fg={color()}>{chromeGlyph("·")} cancelled</text>
|
|
2412
|
+
</Show>
|
|
2413
|
+
<Show when={usage()}>
|
|
2414
|
+
- <text fg={color()}>· {usage()}</text>
|
|
2415
|
+
+ <text fg={color()}>{chromeGlyph("·")} {usage()}</text>
|
|
2416
|
+
</Show>
|
|
2417
|
+
</box>
|
|
2418
|
+
<box border={["top"]} borderColor={color()} flexGrow={1} />
|
|
2419
|
+
@@ -2103,7 +2105,7 @@ function CompactionQueued() {
|
|
2420
|
+
<box flexDirection="row" alignItems="center">
|
|
2421
|
+
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
|
|
2422
|
+
<box flexDirection="row" gap={1} paddingLeft={1} paddingRight={1}>
|
|
2423
|
+
- <text fg={theme.text.muted}>◇</text>
|
|
2424
|
+
+ <text fg={theme.text.muted}>{chromeGlyph("◇")}</text>
|
|
2425
|
+
<text fg={theme.text.muted}>Compaction queued</text>
|
|
2426
|
+
</box>
|
|
2427
|
+
<box border={["top"]} borderColor={theme.border.base} flexGrow={1} />
|
|
2428
|
+
@@ -2369,7 +2371,7 @@ function QueuedPromptDock(props: { prompts: { id: string; text: string }[]; onOp
|
|
2429
|
+
>
|
|
2430
|
+
<text fg={theme.text.muted} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
|
|
2431
|
+
<span style={{ fg: theme.text.base }}>{props.prompts.length} queued</span>
|
|
2432
|
+
- <Show when={next()}>{(text) => <> · {text()}</>}</Show>
|
|
2433
|
+
+ <Show when={next()}>{(text) => <> {chromeGlyph("·")} {text()}</>}</Show>
|
|
2434
|
+
</text>
|
|
2435
|
+
</box>
|
|
2436
|
+
</box>
|
|
2437
|
+
@@ -2394,7 +2396,7 @@ function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
|
|
2438
|
+
{(retry) => (
|
|
2439
|
+
<box paddingLeft={3}>
|
|
2440
|
+
<text fg={theme.text.feedback.warning.base}>
|
|
2441
|
+
- ⚠ {seconds() > 0 ? `Retrying in ${seconds()}s` : "Retry due"} · attempt {retry().attempt} ·{" "}
|
|
2442
|
+
+ {chromeGlyph("⚠")} {seconds() > 0 ? `Retrying in ${seconds()}s` : "Retry due"} {chromeGlyph("·")} attempt {retry().attempt} {chromeGlyph("·")} {" "}
|
|
2443
|
+
{retry().error.message}
|
|
2444
|
+
</text>
|
|
2445
|
+
</box>
|
|
2446
|
+
@@ -2575,7 +2577,7 @@ function GenericTool(props: ToolProps) {
|
|
2447
|
+
return (
|
|
2448
|
+
<>
|
|
2449
|
+
<InlineTool
|
|
2450
|
+
- icon={props.part.state.status === "error" ? "✗" : "✓"}
|
|
2451
|
+
+ icon={chromeGlyph(props.part.state.status === "error" ? "✗" : "✓")}
|
|
2452
|
+
complete={props.part.state.status === "completed"}
|
|
2453
|
+
pending={props.tool}
|
|
2454
|
+
spinner={loading()}
|
|
2455
|
+
@@ -2943,9 +2945,9 @@ function ShellDisplay(props: {
|
|
2456
|
+
when={props.command}
|
|
2457
|
+
fallback={
|
|
2458
|
+
isRunning() || props.status === "streaming" ? (
|
|
2459
|
+
- <Spinner color={color()}>Writing command…</Spinner>
|
|
2460
|
+
+ <Spinner color={color()}>{chromeLabel("Writing command…")}</Spinner>
|
|
2461
|
+
) : (
|
|
2462
|
+
- <text fg={theme.text.muted}>Writing command…</text>
|
|
2463
|
+
+ <text fg={theme.text.muted}>{chromeLabel("Writing command…")}</text>
|
|
2464
|
+
)
|
|
2465
|
+
}
|
|
2466
|
+
>
|
|
2467
|
+
@@ -3014,7 +3016,7 @@ function Write(props: ToolProps) {
|
|
2468
|
+
</BlockTool>
|
|
2469
|
+
</Match>
|
|
2470
|
+
<Match when={true}>
|
|
2471
|
+
- <InlineTool icon="←" pending="Preparing write…" complete={stringValue(props.input.path)} part={props.part}>
|
|
2472
|
+
+ <InlineTool icon={chromeGlyph("←")} pending={chromeLabel("Preparing write…")} complete={stringValue(props.input.path)} part={props.part}>
|
|
2473
|
+
Write {pathFormatter.format(stringValue(props.input.path))}
|
|
2474
|
+
</InlineTool>
|
|
2475
|
+
</Match>
|
|
2476
|
+
@@ -3025,7 +3027,7 @@ function Write(props: ToolProps) {
|
|
2477
|
+
function Glob(props: ToolProps) {
|
|
2478
|
+
const pathFormatter = usePathFormatter()
|
|
2479
|
+
return (
|
|
2480
|
+
- <InlineTool icon="✱" pending="Finding files…" complete={stringValue(props.input.pattern)} part={props.part}>
|
|
2481
|
+
+ <InlineTool icon={chromeGlyph("✱")} pending={chromeLabel("Finding files…")} complete={stringValue(props.input.pattern)} part={props.part}>
|
|
2482
|
+
Glob "{stringValue(props.input.pattern)}"{" "}
|
|
2483
|
+
<Show when={stringValue(props.input.path)}>in {pathFormatter.format(stringValue(props.input.path))} </Show>
|
|
2484
|
+
<Show when={finiteNumber(props.metadata.count)}>
|
|
2485
|
+
@@ -3048,8 +3050,8 @@ function Read(props: ToolProps) {
|
|
2486
|
+
return (
|
|
2487
|
+
<>
|
|
2488
|
+
<InlineTool
|
|
2489
|
+
- icon="→"
|
|
2490
|
+
- pending="Reading file…"
|
|
2491
|
+
+ icon={chromeGlyph("→")}
|
|
2492
|
+
+ pending={chromeLabel("Reading file…")}
|
|
2493
|
+
complete={stringValue(props.input.path)}
|
|
2494
|
+
spinner={isRunning()}
|
|
2495
|
+
part={props.part}
|
|
2496
|
+
@@ -3064,7 +3066,7 @@ function Read(props: ToolProps) {
|
|
2497
|
+
{(filepath) => (
|
|
2498
|
+
<box paddingLeft={3}>
|
|
2499
|
+
<text paddingLeft={3} fg={theme.text.muted}>
|
|
2500
|
+
- ↳ Loaded {pathFormatter.format(filepath)}
|
|
2501
|
+
+ {chromeGlyph("↳")} Loaded {pathFormatter.format(filepath)}
|
|
2502
|
+
</text>
|
|
2503
|
+
</box>
|
|
2504
|
+
)}
|
|
2505
|
+
@@ -3076,7 +3078,7 @@ function Read(props: ToolProps) {
|
|
2506
|
+
function Grep(props: ToolProps) {
|
|
2507
|
+
const pathFormatter = usePathFormatter()
|
|
2508
|
+
return (
|
|
2509
|
+
- <InlineTool icon="✱" pending="Searching content…" complete={stringValue(props.input.pattern)} part={props.part}>
|
|
2510
|
+
+ <InlineTool icon={chromeGlyph("✱")} pending={chromeLabel("Searching content…")} complete={stringValue(props.input.pattern)} part={props.part}>
|
|
2511
|
+
Grep "{stringValue(props.input.pattern)}"{" "}
|
|
2512
|
+
<Show when={stringValue(props.input.path)}>in {pathFormatter.format(stringValue(props.input.path))} </Show>
|
|
2513
|
+
<Show when={finiteNumber(props.metadata.matches)}>
|
|
2514
|
+
@@ -3088,7 +3090,7 @@ function Grep(props: ToolProps) {
|
|
2515
|
+
|
|
2516
|
+
function WebFetch(props: ToolProps) {
|
|
2517
|
+
return (
|
|
2518
|
+
- <InlineTool icon="%" pending="Fetching from the web…" complete={stringValue(props.input.url)} part={props.part}>
|
|
2519
|
+
+ <InlineTool icon="%" pending={chromeLabel("Fetching from the web…")} complete={stringValue(props.input.url)} part={props.part}>
|
|
2520
|
+
WebFetch {stringValue(props.input.url)}
|
|
2521
|
+
</InlineTool>
|
|
2522
|
+
)
|
|
2523
|
+
@@ -3098,7 +3100,7 @@ function WebSearch(props: ToolProps) {
|
|
2524
|
+
const ctx = use()
|
|
2525
|
+
const provider = createMemo(() => stringValue(props.metadata.provider))
|
|
2526
|
+
return (
|
|
2527
|
+
- <InlineTool icon="◈" pending="Searching web…" complete={stringValue(props.input.query)} part={props.part}>
|
|
2528
|
+
+ <InlineTool icon={chromeGlyph("◈")} pending={chromeLabel("Searching web…")} complete={stringValue(props.input.query)} part={props.part}>
|
|
2529
|
+
<Show when={provider()} fallback="Web Search">
|
|
2530
|
+
{(value) => (
|
|
2531
|
+
<>
|
|
2532
|
+
@@ -3133,11 +3135,11 @@ function Subagent(props: ToolProps) {
|
|
2533
|
+
|
|
2534
|
+
return (
|
|
2535
|
+
<InlineTool
|
|
2536
|
+
- icon={continuation() ? "↳" : isRunning() ? "│" : props.part.state.status === "completed" ? "✓" : "│"}
|
|
2537
|
+
+ icon={chromeGlyph(continuation() ? "↳" : isRunning() ? "│" : props.part.state.status === "completed" ? "✓" : "│")}
|
|
2538
|
+
spinner={!continuation() && isRunning()}
|
|
2539
|
+
running={isRunning()}
|
|
2540
|
+
complete={description()}
|
|
2541
|
+
- pending="Delegating…"
|
|
2542
|
+
+ pending={chromeLabel("Delegating…")}
|
|
2543
|
+
part={props.part}
|
|
2544
|
+
onClick={() => {
|
|
2545
|
+
const id = sessionID()
|
|
2546
|
+
@@ -3149,7 +3151,7 @@ function Subagent(props: ToolProps) {
|
|
2547
|
+
) : undefined
|
|
2548
|
+
}
|
|
2549
|
+
>
|
|
2550
|
+
- {`${continuation() ? "Continue subagent" : `${Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General")} Subagent`} — ${description() ?? "Subagent"}${model() ? ` · ${model()}` : ""}`}
|
|
2551
|
+
+ {`${continuation() ? "Continue subagent" : `${Locale.titlecase(stringValue(props.input.agent) ?? stringValue(props.input.subagent_type) ?? "General")} Subagent`} ${chromeGlyph("—")} ${description() ?? "Subagent"}${model() ? ` ${chromeGlyph("·")} ${model()}` : ""}`}
|
|
2552
|
+
</InlineTool>
|
|
2553
|
+
)
|
|
2554
|
+
}
|
|
2555
|
+
@@ -3202,7 +3204,7 @@ function ExecuteCallView(props: { call: Accessor<ExecuteCall> }) {
|
|
2556
|
+
>
|
|
2557
|
+
<box flexDirection="row">
|
|
2558
|
+
<box width={INLINE_TOOL_ICON_WIDTH} flexShrink={0}>
|
|
2559
|
+
- <text fg={color()}>{props.call().status === "error" ? "✗" : "›"}</text>
|
|
2560
|
+
+ <text fg={color()}>{chromeGlyph(props.call().status === "error" ? "✗" : "›")}</text>
|
|
2561
|
+
</box>
|
|
2562
|
+
<text flexGrow={1} wrapMode="none" truncate fg={color()}>
|
|
2563
|
+
{expanded() ? props.call().tool : executeCallSummary(props.call())}
|
|
2564
|
+
@@ -3248,7 +3250,7 @@ function Execute(props: ToolProps) {
|
|
2565
|
+
return (
|
|
2566
|
+
<>
|
|
2567
|
+
<InlineTool
|
|
2568
|
+
- icon={hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│"}
|
|
2569
|
+
+ icon={chromeGlyph(hasRuntimeError() ? "✗" : props.part.state.status === "completed" ? "✓" : "│")}
|
|
2570
|
+
color={hasRuntimeError() ? theme.text.feedback.error.base : undefined}
|
|
2571
|
+
spinner={isLoading()}
|
|
2572
|
+
pending="execute"
|
|
2573
|
+
@@ -3265,7 +3267,7 @@ function Execute(props: ToolProps) {
|
|
2574
|
+
<For each={outputPreview().split("\n")}>
|
|
2575
|
+
{(line, index) => (
|
|
2576
|
+
<text paddingLeft={3} fg={theme.text.feedback.error.base}>
|
|
2577
|
+
- {index() === 0 ? "↳ " : " "}
|
|
2578
|
+
+ {index() === 0 ? `${chromeGlyph("↳")} ` : " "}
|
|
2579
|
+
{line}
|
|
2580
|
+
</text>
|
|
2581
|
+
)}
|
|
2582
|
+
@@ -3297,7 +3299,7 @@ function Edit(props: ToolProps) {
|
|
2583
|
+
<Switch>
|
|
2584
|
+
<Match when={file()}>
|
|
2585
|
+
{(item) => (
|
|
2586
|
+
- <BlockTool path={{ label: "← Edit", value: pathFormatter.format(path()) }} part={props.part}>
|
|
2587
|
+
+ <BlockTool path={{ label: `${chromeGlyph("←")} Edit`, value: pathFormatter.format(path()) }} part={props.part}>
|
|
2588
|
+
<box paddingLeft={1}>
|
|
2589
|
+
<PatchDiff
|
|
2590
|
+
diff={item().patch}
|
|
2591
|
+
@@ -3328,10 +3330,10 @@ function Edit(props: ToolProps) {
|
|
2592
|
+
<BlockTool
|
|
2593
|
+
path={
|
|
2594
|
+
stringValue(props.input.path)
|
|
2595
|
+
- ? { label: "← Edit", value: pathFormatter.format(stringValue(props.input.path)) }
|
|
2596
|
+
+ ? { label: `${chromeGlyph("←")} Edit`, value: pathFormatter.format(stringValue(props.input.path)) }
|
|
2597
|
+
: undefined
|
|
2598
|
+
}
|
|
2599
|
+
- title={stringValue(props.input.path) ? undefined : "# Preparing edit…"}
|
|
2600
|
+
+ title={stringValue(props.input.path) ? undefined : `# ${chromeLabel("Preparing edit…")}`}
|
|
2601
|
+
part={props.part}
|
|
2602
|
+
spinner={props.part.state.status === "streaming"}
|
|
2603
|
+
/>
|
|
2604
|
+
@@ -3375,7 +3377,7 @@ function ApplyPatch(props: ToolProps) {
|
|
2605
|
+
{(file) => (
|
|
2606
|
+
<BlockTool
|
|
2607
|
+
path={{
|
|
2608
|
+
- label: file.type === "add" ? "# Created" : file.type === "delete" ? "# Deleted" : "← Patched",
|
|
2609
|
+
+ label: file.type === "add" ? "# Created" : file.type === "delete" ? "# Deleted" : `${chromeGlyph("←")} Patched`,
|
|
2610
|
+
value: pathFormatter.format(file.relativePath),
|
|
2611
|
+
}}
|
|
2612
|
+
part={props.part}
|
|
2613
|
+
@@ -3422,7 +3424,7 @@ function ApplyPatch(props: ToolProps) {
|
|
2614
|
+
{(file) => (
|
|
2615
|
+
<BlockTool
|
|
2616
|
+
path={{
|
|
2617
|
+
- label: file.type === "add" ? "# Created" : file.type === "delete" ? "# Deleted" : "← Patched",
|
|
2618
|
+
+ label: file.type === "add" ? "# Created" : file.type === "delete" ? "# Deleted" : `${chromeGlyph("←")} Patched`,
|
|
2619
|
+
value: pathFormatter.format(file.resource),
|
|
2620
|
+
}}
|
|
2621
|
+
part={props.part}
|
|
2622
|
+
@@ -3488,7 +3490,7 @@ function Question(props: ToolProps) {
|
|
2623
|
+
</BlockTool>
|
|
2624
|
+
</Match>
|
|
2625
|
+
<Match when={true}>
|
|
2626
|
+
- <InlineTool icon="→" pending="Asking questions…" complete={count()} part={props.part}>
|
|
2627
|
+
+ <InlineTool icon={chromeGlyph("→")} pending={chromeLabel("Asking questions…")} complete={count()} part={props.part}>
|
|
2628
|
+
Asked {count()} question{count() !== 1 ? "s" : ""}
|
|
2629
|
+
</InlineTool>
|
|
2630
|
+
</Match>
|
|
2631
|
+
@@ -3499,7 +3501,7 @@ function Question(props: ToolProps) {
|
|
2632
|
+
function Skill(props: ToolProps) {
|
|
2633
|
+
const name = createMemo(() => stringValue(props.metadata.name) ?? stringValue(props.input.id))
|
|
2634
|
+
return (
|
|
2635
|
+
- <InlineTool icon="→" pending="Loading skill…" complete={name()} part={props.part}>
|
|
2636
|
+
+ <InlineTool icon={chromeGlyph("→")} pending={chromeLabel("Loading skill…")} complete={name()} part={props.part}>
|
|
2637
|
+
Skill "{name()}"
|
|
2638
|
+
</InlineTool>
|
|
2639
|
+
)
|
|
2640
|
+
diff --git a/packages/tui/src/routes/session/location-missing.tsx b/packages/tui/src/routes/session/location-missing.tsx
|
|
2641
|
+
index d42aada29fac26b1dec5372f03941be08aed14d5..15f1d0ec37179a74e1c8d35c1930cc8a97a4d6d5 100644
|
|
2642
|
+
--- a/packages/tui/src/routes/session/location-missing.tsx
|
|
2643
|
+
+++ b/packages/tui/src/routes/session/location-missing.tsx
|
|
2644
|
+
@@ -10,6 +10,7 @@ import { useToast } from "../../ui/toast"
|
|
2645
|
+
import { errorMessage } from "../../util/error"
|
|
2646
|
+
import { DialogWorkspaces, type WorkspaceSelection } from "../../component/dialog-workspaces"
|
|
2647
|
+
import { useData } from "../../context/data"
|
|
2648
|
+
+import { chromeEllipsis } from "../../ui/glyphs"
|
|
2649
|
+
|
|
2650
|
+
export function SessionLocationMissing(props: { directory: string; projectID: string; sessionID: string }) {
|
|
2651
|
+
const dialog = useDialog()
|
|
2652
|
+
@@ -59,7 +60,7 @@ export function SessionLocationMissing(props: { directory: string; projectID: st
|
|
2653
|
+
export function SessionLocationUnavailable(props: { directory: string; onMove: () => void }) {
|
|
2654
|
+
const paths = useTuiPaths()
|
|
2655
|
+
const theme = useTheme()
|
|
2656
|
+
- const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72))
|
|
2657
|
+
+ const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72, chromeEllipsis()))
|
|
2658
|
+
|
|
2659
|
+
return (
|
|
2660
|
+
<SessionQuestion
|
|
2661
|
+
diff --git a/packages/tui/src/routes/session/message-parts.tsx b/packages/tui/src/routes/session/message-parts.tsx
|
|
2662
|
+
index f63ba65250e854f423e0b624e1f3629c750d4a6b..a4b361664eab3078b055f5669bf2a6baf19f5ec2 100644
|
|
2663
|
+
--- a/packages/tui/src/routes/session/message-parts.tsx
|
|
2664
|
+
+++ b/packages/tui/src/routes/session/message-parts.tsx
|
|
2665
|
+
@@ -11,6 +11,7 @@ import { createSyntaxStyleMemo, useTheme, useThemes } from "../../context/theme"
|
|
2666
|
+
import { reasoningSummary } from "../../context/thinking"
|
|
2667
|
+
import { usePlugin } from "../../plugin/context"
|
|
2668
|
+
import { SplitBorder } from "../../ui/border"
|
|
2669
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
2670
|
+
import { Locale } from "../../util/locale"
|
|
2671
|
+
import { use } from "./render-context"
|
|
2672
|
+
import { generateThinkingSyntax } from "./thinking-syntax"
|
|
2673
|
+
@@ -156,7 +157,7 @@ function ReasoningHeader(props: {
|
|
2674
|
+
</Show>
|
|
2675
|
+
<Show when={props.duration}>
|
|
2676
|
+
<span>
|
|
2677
|
+
- {props.title ? " · " : ""}
|
|
2678
|
+
+ {props.title ? ` ${chromeGlyph("·")} ` : ""}
|
|
2679
|
+
{props.duration}
|
|
2680
|
+
</span>
|
|
2681
|
+
</Show>
|
|
2682
|
+
@@ -237,7 +238,7 @@ export function InlineToolRow(props: {
|
|
2683
|
+
fg={props.failed ? props.errorColor : (props.iconColor ?? props.color)}
|
|
2684
|
+
attributes={props.denied ? TextAttributes.STRIKETHROUGH : undefined}
|
|
2685
|
+
>
|
|
2686
|
+
- {props.icon}
|
|
2687
|
+
+ {chromeGlyph(props.icon)}
|
|
2688
|
+
</text>
|
|
2689
|
+
<Show
|
|
2690
|
+
when={props.status}
|
|
2691
|
+
diff --git a/packages/tui/src/routes/session/permission.tsx b/packages/tui/src/routes/session/permission.tsx
|
|
2692
|
+
index 07338aba69d81e27d5213e61131f7d2db0e2ff0a..812d3e97e6fa3085113e9af310b03fd2b5d64161 100644
|
|
2693
|
+
--- a/packages/tui/src/routes/session/permission.tsx
|
|
2694
|
+
+++ b/packages/tui/src/routes/session/permission.tsx
|
|
2695
|
+
@@ -5,6 +5,7 @@ import type { TextareaRenderable } from "@opentui/core"
|
|
2696
|
+
import { useTheme, useThemes } from "../../context/theme"
|
|
2697
|
+
import type { PermissionReply, PermissionRequest } from "@opencode/client"
|
|
2698
|
+
import { SplitBorder } from "../../ui/border"
|
|
2699
|
+
+import { chromeGlyph } from "../../ui/glyphs"
|
|
2700
|
+
import { useData } from "../../context/data"
|
|
2701
|
+
import { filetype } from "../../util/filetype"
|
|
2702
|
+
import { permissionAlwaysLines, permissionOptionLabel, permissionPresentation } from "../../util/permission"
|
|
2703
|
+
@@ -200,7 +201,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
|
2704
|
+
const header = () => (
|
|
2705
|
+
<box flexDirection="column" gap={0}>
|
|
2706
|
+
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
2707
|
+
- <text fg={theme.text.feedback.warning.base}>{"△"}</text>
|
|
2708
|
+
+ <text fg={theme.text.feedback.warning.base}>{chromeGlyph("△")}</text>
|
|
2709
|
+
<text fg={theme.text.base}>Permission required</text>
|
|
2710
|
+
</box>
|
|
2711
|
+
<Show when={props.request.action !== "shell" && current.title}>
|
|
2712
|
+
@@ -321,7 +322,7 @@ function RejectPrompt(props: {
|
|
2713
|
+
>
|
|
2714
|
+
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
|
2715
|
+
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
2716
|
+
- <text fg={theme.text.feedback.error.base}>{"△"}</text>
|
|
2717
|
+
+ <text fg={theme.text.feedback.error.base}>{chromeGlyph("△")}</text>
|
|
2718
|
+
<text fg={theme.text.base}>Reject permission</text>
|
|
2719
|
+
</box>
|
|
2720
|
+
<box paddingLeft={1}>
|
|
2721
|
+
@@ -527,7 +528,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
|
|
2722
|
+
when={props.header}
|
|
2723
|
+
fallback={
|
|
2724
|
+
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
|
2725
|
+
- <text fg={theme.text.feedback.warning.base}>{"△"}</text>
|
|
2726
|
+
+ <text fg={theme.text.feedback.warning.base}>{chromeGlyph("△")}</text>
|
|
2727
|
+
<text fg={theme.text.base}>{props.title}</text>
|
|
2728
|
+
</box>
|
|
2729
|
+
}
|
|
2730
|
+
@@ -603,7 +604,7 @@ export function SessionQuestion<const T extends Record<string, string>>(props: {
|
|
2731
|
+
</Show>
|
|
2732
|
+
<Show when={keys.length > 1}>
|
|
2733
|
+
<text fg={theme.text.base}>
|
|
2734
|
+
- {"⇆"} <span style={{ fg: theme.text.muted }}>select</span>
|
|
2735
|
+
+ {chromeGlyph("⇆")} <span style={{ fg: theme.text.muted }}>select</span>
|
|
2736
|
+
</text>
|
|
2737
|
+
</Show>
|
|
2738
|
+
<text fg={theme.text.base}>
|
|
2739
|
+
diff --git a/packages/tui/src/ui/border.ts b/packages/tui/src/ui/border.ts
|
|
2740
|
+
index 333071020c4b3ce74aad47ec9ba758a566761565..d03cce890838bae1df70d0b9bdab06bbf8131e46 100644
|
|
2741
|
+
--- a/packages/tui/src/ui/border.ts
|
|
2742
|
+
+++ b/packages/tui/src/ui/border.ts
|
|
2743
|
+
@@ -1,3 +1,5 @@
|
|
2744
|
+
+import { chromeBorderChars } from "./glyphs"
|
|
2745
|
+
+
|
|
2746
|
+
export const EmptyBorder = {
|
|
2747
|
+
topLeft: "",
|
|
2748
|
+
bottomLeft: "",
|
|
2749
|
+
@@ -14,8 +16,8 @@ export const EmptyBorder = {
|
|
2750
|
+
|
|
2751
|
+
export const SplitBorder = {
|
|
2752
|
+
border: ["left" as const, "right" as const],
|
|
2753
|
+
- customBorderChars: {
|
|
2754
|
+
- ...EmptyBorder,
|
|
2755
|
+
- vertical: "┃",
|
|
2756
|
+
+ get customBorderChars() {
|
|
2757
|
+
+ const chars = chromeBorderChars()
|
|
2758
|
+
+ return chars ? { ...EmptyBorder, ...chars } : { ...EmptyBorder, vertical: "┃" }
|
|
2759
|
+
},
|
|
2760
|
+
}
|
|
2761
|
+
diff --git a/packages/tui/src/ui/dialog-export-options.tsx b/packages/tui/src/ui/dialog-export-options.tsx
|
|
2762
|
+
index 2a38f6292279460734430cd74894b8ed53056422..a60018289a09ce7c68f1e782a44cb408b35101f1 100644
|
|
2763
|
+
--- a/packages/tui/src/ui/dialog-export-options.tsx
|
|
2764
|
+
+++ b/packages/tui/src/ui/dialog-export-options.tsx
|
|
2765
|
+
@@ -1,6 +1,7 @@
|
|
2766
|
+
import { Keymap } from "../context/keymap"
|
|
2767
|
+
import { useNewDesigns } from "../config"
|
|
2768
|
+
import { useTheme } from "../context/theme"
|
|
2769
|
+
+import { chromeGlyph } from "./glyphs"
|
|
2770
|
+
import { DialogHeader, DialogLayout, type DialogContext } from "./dialog"
|
|
2771
|
+
import { createStore } from "solid-js/store"
|
|
2772
|
+
import { For, Show } from "solid-js"
|
|
2773
|
+
@@ -153,7 +154,7 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|
2774
|
+
: theme.text.formfield.base
|
|
2775
|
+
}
|
|
2776
|
+
>
|
|
2777
|
+
- {store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
|
2778
|
+
+ {store.format === format ? chromeGlyph("◉") : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
|
2779
|
+
</text>
|
|
2780
|
+
</box>
|
|
2781
|
+
)}
|
|
2782
|
+
diff --git a/packages/tui/src/ui/dialog-export-result.tsx b/packages/tui/src/ui/dialog-export-result.tsx
|
|
2783
|
+
index 6304fbdb307b218f13945547a81ff9faed281975..4751cc8d57c60f60729cba79acc21851852948a1 100644
|
|
2784
|
+
--- a/packages/tui/src/ui/dialog-export-result.tsx
|
|
2785
|
+
+++ b/packages/tui/src/ui/dialog-export-result.tsx
|
|
2786
|
+
@@ -3,6 +3,7 @@ import { useNewDesigns } from "../config"
|
|
2787
|
+
import { useClipboard } from "../context/clipboard"
|
|
2788
|
+
import { Keymap } from "../context/keymap"
|
|
2789
|
+
import { useTheme } from "../context/theme"
|
|
2790
|
+
+import { chromeGlyph } from "./glyphs"
|
|
2791
|
+
import { DialogHeader, DialogLayout, useDialog, type DialogContext } from "./dialog"
|
|
2792
|
+
import { useToast } from "./toast"
|
|
2793
|
+
|
|
2794
|
+
@@ -51,7 +52,7 @@ export function DialogExportResult(props: { path: string; onClose?: () => void }
|
|
2795
|
+
actions: {
|
|
2796
|
+
primary: [
|
|
2797
|
+
copied()
|
|
2798
|
+
- ? { title: "✓ copied", color: theme.text.feedback.success.base, onTrigger: copy }
|
|
2799
|
+
+ ? { title: `${chromeGlyph("✓")} copied`, color: theme.text.feedback.success.base, onTrigger: copy }
|
|
2800
|
+
: { title: "copy path", key: "c", onTrigger: copy },
|
|
2801
|
+
],
|
|
2802
|
+
},
|
|
2803
|
+
diff --git a/packages/tui/src/ui/dialog-prompt.tsx b/packages/tui/src/ui/dialog-prompt.tsx
|
|
2804
|
+
index b85074c491e86c1e34b646db5b09697df43b9950..00badea28607a4cc3471c906d6588f7d13cd8e20 100644
|
|
2805
|
+
--- a/packages/tui/src/ui/dialog-prompt.tsx
|
|
2806
|
+
+++ b/packages/tui/src/ui/dialog-prompt.tsx
|
|
2807
|
+
@@ -6,6 +6,7 @@ import { Show, createEffect, createSignal, onMount, type JSX } from "solid-js"
|
|
2808
|
+
import { Spinner } from "../component/spinner"
|
|
2809
|
+
import { useConfig, useNewDesigns } from "../config"
|
|
2810
|
+
import { useRenderer } from "@opentui/solid"
|
|
2811
|
+
+import { chromeLabel } from "./glyphs"
|
|
2812
|
+
|
|
2813
|
+
export type DialogPromptAction = {
|
|
2814
|
+
title: string
|
|
2815
|
+
@@ -148,7 +149,7 @@ export function DialogPrompt(props: DialogPromptProps) {
|
|
2816
|
+
},
|
|
2817
|
+
}}
|
|
2818
|
+
legacyFooter={
|
|
2819
|
+
- <Show when={!props.busy} fallback={<text fg={theme.text.muted}>processing…</text>}>
|
|
2820
|
+
+ <Show when={!props.busy} fallback={<text fg={theme.text.muted}>{chromeLabel("processing…")}</text>}>
|
|
2821
|
+
<DialogFooter
|
|
2822
|
+
actions={{
|
|
2823
|
+
secondary: [{ title: "submit", command: "dialog.prompt.submit", onTrigger: confirm }, ...extraActions()],
|
|
2824
|
+
@@ -175,7 +176,7 @@ export function DialogPrompt(props: DialogPromptProps) {
|
|
2825
|
+
cursorStyle={config.cursor}
|
|
2826
|
+
/>
|
|
2827
|
+
<Show when={props.busy}>
|
|
2828
|
+
- <Spinner color={theme.text.muted}>{props.busyText ?? "Working…"}</Spinner>
|
|
2829
|
+
+ <Spinner color={theme.text.muted}>{props.busyText ?? chromeLabel("Working…")}</Spinner>
|
|
2830
|
+
</Show>
|
|
2831
|
+
</box>
|
|
2832
|
+
</DialogLayout>
|
|
2833
|
+
diff --git a/packages/tui/src/ui/dialog-select.tsx b/packages/tui/src/ui/dialog-select.tsx
|
|
2834
|
+
index f86d8b507f79920ac75de168f1cd1b1076a70380..acf4b6d5285c7478182c845f6847fc95d4a17c58 100644
|
|
2835
|
+
--- a/packages/tui/src/ui/dialog-select.tsx
|
|
2836
|
+
+++ b/packages/tui/src/ui/dialog-select.tsx
|
|
2837
|
+
@@ -22,6 +22,7 @@ import { Locale } from "../util/locale"
|
|
2838
|
+
import { getScrollAcceleration } from "../util/scroll"
|
|
2839
|
+
import { useConfig, useNewDesigns } from "../config"
|
|
2840
|
+
import { moveSelection, reconcileSelection } from "./select-controller"
|
|
2841
|
+
+import { chromeEllipsis, chromeGlyph } from "./glyphs"
|
|
2842
|
+
|
|
2843
|
+
export interface DialogSelectProps<T> {
|
|
2844
|
+
title: string
|
|
2845
|
+
@@ -789,7 +790,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
|
|
2846
|
+
</scrollbox>
|
|
2847
|
+
<Show when={overflows()}>
|
|
2848
|
+
<box paddingLeft={DIALOG_LIST_INSET} flexShrink={0}>
|
|
2849
|
+
- <text fg={theme.text.faint}>{moreBelow() ? "…" : " "}</text>
|
|
2850
|
+
+ <text fg={theme.text.faint}>{moreBelow() ? chromeGlyph("…") : " "}</text>
|
|
2851
|
+
</box>
|
|
2852
|
+
</Show>
|
|
2853
|
+
</Show>
|
|
2854
|
+
@@ -855,7 +856,7 @@ function DialogSelectRow(props: {
|
|
2855
|
+
</Show>
|
|
2856
|
+
<Show when={props.current && !props.gutter}>
|
|
2857
|
+
<text flexShrink={0} fg={newDesigns() ? theme.text.formfield.selected : text()}>
|
|
2858
|
+
- {newDesigns() ? "▪" : "●"}
|
|
2859
|
+
+ {chromeGlyph(newDesigns() ? "▪" : "●")}
|
|
2860
|
+
</text>
|
|
2861
|
+
</Show>
|
|
2862
|
+
<Show when={props.gutter}>
|
|
2863
|
+
@@ -873,8 +874,8 @@ function DialogSelectRow(props: {
|
|
2864
|
+
(props.truncateTitle === false
|
|
2865
|
+
? props.title
|
|
2866
|
+
: props.truncateTitle === "left"
|
|
2867
|
+
- ? Locale.truncateLeft(props.title, props.titleWidth ?? 61)
|
|
2868
|
+
- : Locale.truncate(props.title, props.titleWidth ?? 61))}
|
|
2869
|
+
+ ? Locale.truncateLeft(props.title, props.titleWidth ?? 61, chromeEllipsis())
|
|
2870
|
+
+ : Locale.truncate(props.title, props.titleWidth ?? 61, chromeEllipsis()))}
|
|
2871
|
+
<Show when={props.description}>
|
|
2872
|
+
<span style={{ fg: props.active ? secondary() : theme.text.muted }}>{" " + props.description}</span>
|
|
2873
|
+
</Show>
|
|
2874
|
+
@@ -891,7 +892,7 @@ function DialogSelectRow(props: {
|
|
2875
|
+
<Show when={newDesigns()} fallback={row()}>
|
|
2876
|
+
<box flexDirection="row">
|
|
2877
|
+
<text width={1} flexShrink={0} fg={theme.text.formfield.focused}>
|
|
2878
|
+
- {props.active ? "┃" : " "}
|
|
2879
|
+
+ {props.active ? chromeGlyph("┃") : " "}
|
|
2880
|
+
</text>
|
|
2881
|
+
{row()}
|
|
2882
|
+
</box>
|
|
2883
|
+
@@ -913,7 +914,7 @@ function DialogSelectDetails(props: { details?: string[] }) {
|
|
2884
|
+
paddingRight={DIALOG_LIST_INSET - (newDesigns() ? 0 : 1)}
|
|
2885
|
+
>
|
|
2886
|
+
<text fg={theme.text.muted} wrapMode="none">
|
|
2887
|
+
- {Locale.truncateMiddle(detail, Math.max(1, width()))}
|
|
2888
|
+
+ {Locale.truncateMiddle(detail, Math.max(1, width()), chromeEllipsis())}
|
|
2889
|
+
</text>
|
|
2890
|
+
</box>
|
|
2891
|
+
)}
|
|
2892
|
+
diff --git a/packages/tui/src/ui/file-path.tsx b/packages/tui/src/ui/file-path.tsx
|
|
2893
|
+
index 09b38066707cdfcf1aa3e8dd55cee47236b5888c..5fc5dd57257c9fc88855b1760761308404f51eff 100644
|
|
2894
|
+
--- a/packages/tui/src/ui/file-path.tsx
|
|
2895
|
+
+++ b/packages/tui/src/ui/file-path.tsx
|
|
2896
|
+
@@ -1,6 +1,7 @@
|
|
2897
|
+
import type { RGBA } from "@opentui/core"
|
|
2898
|
+
import { createMemo } from "solid-js"
|
|
2899
|
+
import { stringWidth } from "../util/string-width"
|
|
2900
|
+
+import { chromeEllipsis } from "./glyphs"
|
|
2901
|
+
|
|
2902
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
2903
|
+
|
|
2904
|
+
@@ -29,6 +30,7 @@ export function FilePath(props: FilePathProps) {
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
export function truncateFilePath(value: string, maxWidth: number) {
|
|
2908
|
+
+ const ellipsis = chromeEllipsis()
|
|
2909
|
+
if (maxWidth <= 0) return ""
|
|
2910
|
+
if (stringWidth(value) <= maxWidth) return value
|
|
2911
|
+
|
|
2912
|
+
@@ -52,7 +54,7 @@ export function truncateFilePath(value: string, maxWidth: number) {
|
|
2913
|
+
return root + truncateBasename(basename, maxWidth - rootWidth)
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
- const prefix = `${root}…${separator}`
|
|
2917
|
+
+ const prefix = `${root}${ellipsis}${separator}`
|
|
2918
|
+
const basenameWidth = maxWidth - stringWidth(prefix)
|
|
2919
|
+
if (basenameWidth <= 0) return takeStart(prefix, maxWidth)
|
|
2920
|
+
const compact = truncateBasename(basename, basenameWidth)
|
|
2921
|
+
@@ -66,7 +68,7 @@ export function truncateFilePath(value: string, maxWidth: number) {
|
|
2922
|
+
const next = stringWidth(segment) + separatorWidth
|
|
2923
|
+
if (width + next > maxWidth) {
|
|
2924
|
+
const available = maxWidth - width - separatorWidth
|
|
2925
|
+
- if (available > 1) selected.unshift(takeStart(segment, available - 1) + "…")
|
|
2926
|
+
+ if (available > stringWidth(ellipsis)) selected.unshift(takeStart(segment, available - stringWidth(ellipsis)) + ellipsis)
|
|
2927
|
+
break
|
|
2928
|
+
}
|
|
2929
|
+
selected.unshift(segment)
|
|
2930
|
+
@@ -77,15 +79,17 @@ export function truncateFilePath(value: string, maxWidth: number) {
|
|
2931
|
+
|
|
2932
|
+
function truncateBasename(value: string, maxWidth: number) {
|
|
2933
|
+
if (stringWidth(value) <= maxWidth) return value
|
|
2934
|
+
- if (maxWidth <= 1) return takeStart("…", maxWidth)
|
|
2935
|
+
+ const ellipsis = chromeEllipsis()
|
|
2936
|
+
+ const markerWidth = stringWidth(ellipsis)
|
|
2937
|
+
+ if (maxWidth <= markerWidth) return takeStart(ellipsis, maxWidth)
|
|
2938
|
+
|
|
2939
|
+
const dot = value.lastIndexOf(".")
|
|
2940
|
+
const extension = dot > 0 ? value.slice(dot) : ""
|
|
2941
|
+
const extensionWidth = stringWidth(extension)
|
|
2942
|
+
- if (extensionWidth >= maxWidth) return "…" + takeEnd(extension, maxWidth - 1)
|
|
2943
|
+
+ if (extensionWidth + markerWidth >= maxWidth) return ellipsis + takeEnd(extension, maxWidth - markerWidth)
|
|
2944
|
+
|
|
2945
|
+
const stem = extension ? value.slice(0, dot) : value
|
|
2946
|
+
- return takeStart(stem, maxWidth - extensionWidth - 1) + "…" + extension
|
|
2947
|
+
+ return takeStart(stem, maxWidth - extensionWidth - markerWidth) + ellipsis + extension
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function takeStart(value: string, maxWidth: number) {
|
|
2951
|
+
diff --git a/packages/tui/src/ui/glyphs.ts b/packages/tui/src/ui/glyphs.ts
|
|
2952
|
+
new file mode 100644
|
|
2953
|
+
index 0000000000000000000000000000000000000000..f20313f4ce974158bdcca422dc7fc812a6091308
|
|
2954
|
+
--- /dev/null
|
|
2955
|
+
+++ b/packages/tui/src/ui/glyphs.ts
|
|
2956
|
+
@@ -0,0 +1,188 @@
|
|
2957
|
+
+/** Host chrome glyphs. This deliberately does not transform arbitrary text. */
|
|
2958
|
+
+export type GlyphMode = "auto" | "unicode" | "ascii"
|
|
2959
|
+
+
|
|
2960
|
+
+export function glyphMode(value = process.env.CTG_TUI_GLYPHS): GlyphMode {
|
|
2961
|
+
+ if (value === "ascii" || value === "unicode" || value === "auto") return value
|
|
2962
|
+
+ return "unicode"
|
|
2963
|
+
+}
|
|
2964
|
+
+
|
|
2965
|
+
+export function asciiGlyphs() {
|
|
2966
|
+
+ return glyphMode() === "ascii"
|
|
2967
|
+
+}
|
|
2968
|
+
+
|
|
2969
|
+
+/** Replace known OpenCode chrome icons, leaving unknown/user supplied strings unchanged. */
|
|
2970
|
+
+export function chromeGlyph(value: string) {
|
|
2971
|
+
+ if (!asciiGlyphs()) return value
|
|
2972
|
+
+ const replacements: Record<string, string> = {
|
|
2973
|
+
+ "→": ">",
|
|
2974
|
+
+ "←": "<",
|
|
2975
|
+
+ "↳": "`",
|
|
2976
|
+
+ "↻": "~",
|
|
2977
|
+
+ "⟳": "~",
|
|
2978
|
+
+ "✱": "*",
|
|
2979
|
+
+ "◈": "@",
|
|
2980
|
+
+ "⚙": "#",
|
|
2981
|
+
+ "✓": "+",
|
|
2982
|
+
+ "✗": "x",
|
|
2983
|
+
+ "×": "x",
|
|
2984
|
+
+ "•": "*",
|
|
2985
|
+
+ "●": "*",
|
|
2986
|
+
+ "▪": "*",
|
|
2987
|
+
+ "■": "#",
|
|
2988
|
+
+ "◉": "o",
|
|
2989
|
+
+ "○": "o",
|
|
2990
|
+
+ "⇆": "<>",
|
|
2991
|
+
+ "✕": "x",
|
|
2992
|
+
+ "⦿": "o",
|
|
2993
|
+
+ "‹": "<",
|
|
2994
|
+
+ "⌕": "?",
|
|
2995
|
+
+ "⊙": "*",
|
|
2996
|
+
+ "⬖": "*",
|
|
2997
|
+
+ "⎇": "*",
|
|
2998
|
+
+ "≡": "=",
|
|
2999
|
+
+ "▸": ">",
|
|
3000
|
+
+ "▾": "v",
|
|
3001
|
+
+ "▼": "v",
|
|
3002
|
+
+ "▶": ">",
|
|
3003
|
+
+ "⚠": "!",
|
|
3004
|
+
+ "◇": "o",
|
|
3005
|
+
+ "△": "!",
|
|
3006
|
+
+ "›": ">",
|
|
3007
|
+
+ "┃": "|",
|
|
3008
|
+
+ "│": "|",
|
|
3009
|
+
+ "↑": "^",
|
|
3010
|
+
+ "↓": "v",
|
|
3011
|
+
+ "├": "|-",
|
|
3012
|
+
+ "└": "`-",
|
|
3013
|
+
+ "─": "-",
|
|
3014
|
+
+ "…": "...",
|
|
3015
|
+
+ "·": ".",
|
|
3016
|
+
+ "−": "-",
|
|
3017
|
+
+ "—": "-",
|
|
3018
|
+
+ "’": "'",
|
|
3019
|
+
+ }
|
|
3020
|
+
+ return replacements[value] ?? value
|
|
3021
|
+
+}
|
|
3022
|
+
+
|
|
3023
|
+
+/** Static host copy can opt in separately; dynamic/user text is deliberately not normalized. */
|
|
3024
|
+
+const ASCII_CHROME_LABELS: Record<string, string> = {
|
|
3025
|
+
+ "Searching…": "Searching...",
|
|
3026
|
+
+ "Run a command…": "Run a command...",
|
|
3027
|
+
+ "Ask anything…": "Ask anything...",
|
|
3028
|
+
+ "Search worktrees…": "Search worktrees...",
|
|
3029
|
+
+ "Search sessions and projects…": "Search sessions and projects...",
|
|
3030
|
+
+ "Connecting …": "Connecting ...",
|
|
3031
|
+
+ "Connecting…": "Connecting...",
|
|
3032
|
+
+ "Searching sessions…": "Searching sessions...",
|
|
3033
|
+
+ "Loading sessions…": "Loading sessions...",
|
|
3034
|
+
+ "Writing debug snapshot…": "Writing debug snapshot...",
|
|
3035
|
+
+ "Checking for updates…": "Checking for updates...",
|
|
3036
|
+
+ "Loading skills…": "Loading skills...",
|
|
3037
|
+
+ "Gathering your stats…": "Gathering your stats...",
|
|
3038
|
+
+ "Restarting service…": "Restarting service...",
|
|
3039
|
+
+ "Connection lost…": "Connection lost...",
|
|
3040
|
+
+ "Receiving code…": "Receiving code...",
|
|
3041
|
+
+ "Waiting for code…": "Waiting for code...",
|
|
3042
|
+
+ "Waiting for output…": "Waiting for output...",
|
|
3043
|
+
+ "Loading worktrees…": "Loading worktrees...",
|
|
3044
|
+
+ "Refreshing sessions and projects…": "Refreshing sessions and projects...",
|
|
3045
|
+
+ "Searching sessions and projects…": "Searching sessions and projects...",
|
|
3046
|
+
+ "Creating worktree…": "Creating worktree...",
|
|
3047
|
+
+ "Loading server information…": "Loading server information...",
|
|
3048
|
+
+ "Starting command…": "Starting command...",
|
|
3049
|
+
+ "Waiting for command to finish…": "Waiting for command to finish...",
|
|
3050
|
+
+ "Starting authorization…": "Starting authorization...",
|
|
3051
|
+
+ "Waiting for authorization…": "Waiting for authorization...",
|
|
3052
|
+
+ "Opening link…": "Opening link...",
|
|
3053
|
+
+ "Finishing startup…": "Finishing startup...",
|
|
3054
|
+
+ "Loading plugins…": "Loading plugins...",
|
|
3055
|
+
+ "Loading image…": "Loading image...",
|
|
3056
|
+
+ "Loading session history…": "Loading session history...",
|
|
3057
|
+
+ "Writing command…": "Writing command...",
|
|
3058
|
+
+ "Fetching from the web…": "Fetching from the web...",
|
|
3059
|
+
+ "Delegating…": "Delegating...",
|
|
3060
|
+
+ "Preparing edit…": "Preparing edit...",
|
|
3061
|
+
+ "Forking session…": "Forking session...",
|
|
3062
|
+
+ "Installing OpenCode …": "Installing OpenCode ...",
|
|
3063
|
+
+ "Resolving diff…": "Resolving diff...",
|
|
3064
|
+
+ "Loading branches…": "Loading branches...",
|
|
3065
|
+
+ "Choose…": "Choose...",
|
|
3066
|
+
+ "Loading diff…": "Loading diff...",
|
|
3067
|
+
+ "processing…": "processing...",
|
|
3068
|
+
+ "Working…": "Working...",
|
|
3069
|
+
+ "Search sessions…": "Search sessions...",
|
|
3070
|
+
+ "Loading output…": "Loading output...",
|
|
3071
|
+
+ "Searching content…": "Searching content...",
|
|
3072
|
+
+ "Reading file…": "Reading file...",
|
|
3073
|
+
+ "Preparing write…": "Preparing write...",
|
|
3074
|
+
+ "Finding files…": "Finding files...",
|
|
3075
|
+
+ "Searching web…": "Searching web...",
|
|
3076
|
+
+ "Asking questions…": "Asking questions...",
|
|
3077
|
+
+ "Loading skill…": "Loading skill...",
|
|
3078
|
+
+ "Waiting for permission event…": "Waiting for permission event...",
|
|
3079
|
+
+ "Waiting…": "Waiting...",
|
|
3080
|
+
+ "No matching files…": "No matching files...",
|
|
3081
|
+
+ "Reloading configuration…": "Reloading configuration...",
|
|
3082
|
+
+ "Unable to read shell output. Retrying…": "Unable to read shell output. Retrying...",
|
|
3083
|
+
+ "Thinking…": "Thinking...",
|
|
3084
|
+
+ "Running command…": "Running command...",
|
|
3085
|
+
+ "Preparing command…": "Preparing command...",
|
|
3086
|
+
+ "Running code…": "Running code...",
|
|
3087
|
+
+ "Preparing code…": "Preparing code...",
|
|
3088
|
+
+ "Report copied — paste it into a new GitHub issue.": "Report copied - paste it into a new GitHub issue.",
|
|
3089
|
+
+}
|
|
3090
|
+
+
|
|
3091
|
+
+export function chromeLabel(value: string) {
|
|
3092
|
+
+ if (!asciiGlyphs()) return value
|
|
3093
|
+
+ return ASCII_CHROME_LABELS[value] ?? value
|
|
3094
|
+
+}
|
|
3095
|
+
+
|
|
3096
|
+
+export function chromeEllipsis() {
|
|
3097
|
+
+ return asciiGlyphs() ? "..." : "…"
|
|
3098
|
+
+}
|
|
3099
|
+
+
|
|
3100
|
+
+export function chromeStatusSeparator() {
|
|
3101
|
+
+ return asciiGlyphs() ? "|" : "·"
|
|
3102
|
+
+}
|
|
3103
|
+
+
|
|
3104
|
+
+export const ASCII_SPINNER_FRAMES = ["-", "\\", "|", "/"]
|
|
3105
|
+
+
|
|
3106
|
+
+export const ASCII_BORDER_CHARS = {
|
|
3107
|
+
+ topLeft: "+",
|
|
3108
|
+
+ bottomLeft: "+",
|
|
3109
|
+
+ vertical: "|",
|
|
3110
|
+
+ topRight: "+",
|
|
3111
|
+
+ bottomRight: "+",
|
|
3112
|
+
+ horizontal: "-",
|
|
3113
|
+
+ bottomT: "+",
|
|
3114
|
+
+ topT: "+",
|
|
3115
|
+
+ cross: "+",
|
|
3116
|
+
+ leftT: "+",
|
|
3117
|
+
+ rightT: "+",
|
|
3118
|
+
+}
|
|
3119
|
+
+
|
|
3120
|
+
+export function chromeBorderChars() {
|
|
3121
|
+
+ return asciiGlyphs() ? { ...ASCII_BORDER_CHARS } : undefined
|
|
3122
|
+
+}
|
|
3123
|
+
+
|
|
3124
|
+
+export const CHROME_GLYPHS = {
|
|
3125
|
+
+ action: "→",
|
|
3126
|
+
+ back: "←",
|
|
3127
|
+
+ continuation: "↳",
|
|
3128
|
+
+ reconnect: "↻",
|
|
3129
|
+
+ search: "✱",
|
|
3130
|
+
+ web: "◈",
|
|
3131
|
+
+ tool: "⚙",
|
|
3132
|
+
+ success: "✓",
|
|
3133
|
+
+ error: "✗",
|
|
3134
|
+
+ pending: "•",
|
|
3135
|
+
+ selection: "▪",
|
|
3136
|
+
+ permission: "△",
|
|
3137
|
+
+ border: "┃",
|
|
3138
|
+
+} as const
|
|
3139
|
+
+
|
|
3140
|
+
+export const ASCII_LOGO = {
|
|
3141
|
+
+ left: [" ", " O P E N C O D E ", " O P E N C O D E ", " O P E N C O D E "],
|
|
3142
|
+
+ right: [" ", " O P E N C O D E ", " O P E N C O D E ", " O P E N C O D E "],
|
|
3143
|
+
+ compact: [" ", " OC ", " OC ", " OC "],
|
|
3144
|
+
+} as const
|
|
3145
|
+
diff --git a/packages/tui/src/ui/spinner.ts b/packages/tui/src/ui/spinner.ts
|
|
3146
|
+
index c185ea7b833f2087b4c925aa40feb0b72239a19e..83aa3e97230f3522832f2125bb43c292ae8d4951 100644
|
|
3147
|
+
--- a/packages/tui/src/ui/spinner.ts
|
|
3148
|
+
+++ b/packages/tui/src/ui/spinner.ts
|
|
3149
|
+
@@ -1,5 +1,6 @@
|
|
3150
|
+
import type { ColorInput } from "@opentui/core"
|
|
3151
|
+
import { RGBA } from "@opentui/core"
|
|
3152
|
+
+import { asciiGlyphs } from "./glyphs"
|
|
3153
|
+
import type { ColorGenerator } from "opentui-spinner"
|
|
3154
|
+
|
|
3155
|
+
interface AdvancedGradientOptions {
|
|
3156
|
+
@@ -311,17 +312,17 @@ export function createFrames(options: KnightRiderOptions = {}): string[] {
|
|
3157
|
+
const index = calculateColorIndex(frameIndex, charIndex, width, trailOptions)
|
|
3158
|
+
|
|
3159
|
+
if (style === "diamonds") {
|
|
3160
|
+
- const shapes = ["⬥", "◆", "⬩", "⬪"]
|
|
3161
|
+
+ const shapes = asciiGlyphs() ? ["*", "#", "o", "+"] : ["⬥", "◆", "⬩", "⬪"]
|
|
3162
|
+
if (index >= 0 && index < trailOptions.colors.length) {
|
|
3163
|
+
return shapes[Math.min(index, shapes.length - 1)]
|
|
3164
|
+
}
|
|
3165
|
+
- return "·"
|
|
3166
|
+
+ return asciiGlyphs() ? "." : "·"
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
// Default to blocks
|
|
3170
|
+
// It's active if we have a valid color index that is within our colors array
|
|
3171
|
+
const isActive = index >= 0 && index < trailOptions.colors.length
|
|
3172
|
+
- return isActive ? "■" : "⬝"
|
|
3173
|
+
+ return isActive ? (asciiGlyphs() ? "#" : "■") : asciiGlyphs() ? "." : "⬝"
|
|
3174
|
+
}).join("")
|
|
3175
|
+
})
|
|
3176
|
+
|
|
3177
|
+
diff --git a/packages/tui/src/ui/toast.tsx b/packages/tui/src/ui/toast.tsx
|
|
3178
|
+
index 8f44284c2fae13fb1a897d4950416fa3b918c578..b6da5b2f676f2e0502044e69f365c4e557c73f3c 100644
|
|
3179
|
+
--- a/packages/tui/src/ui/toast.tsx
|
|
3180
|
+
+++ b/packages/tui/src/ui/toast.tsx
|
|
3181
|
+
@@ -1,6 +1,7 @@
|
|
3182
|
+
import { createContext, createSignal, onCleanup, useContext, type ParentProps, Show } from "solid-js"
|
|
3183
|
+
import { createStore } from "solid-js/store"
|
|
3184
|
+
import { useTheme } from "../context/theme"
|
|
3185
|
+
+import { chromeGlyph } from "./glyphs"
|
|
3186
|
+
import { useRenderer, useTerminalDimensions } from "@opentui/solid"
|
|
3187
|
+
import { SplitBorder } from "./border"
|
|
3188
|
+
import { TextAttributes } from "@opentui/core"
|
|
3189
|
+
@@ -39,7 +40,7 @@ function ToastSurface(props: {
|
|
3190
|
+
attributes={hovered() && props.toast.action ? TextAttributes.BOLD : undefined}
|
|
3191
|
+
fg={hovered() ? theme.text.action.primary.base : theme.text.muted}
|
|
3192
|
+
>
|
|
3193
|
+
- {props.toast.action ? `› ${props.toast.action.label}` : "x"}
|
|
3194
|
+
+ {props.toast.action ? `${chromeGlyph("›")} ${props.toast.action.label}` : "x"}
|
|
3195
|
+
</text>
|
|
3196
|
+
)
|
|
3197
|
+
|
|
3198
|
+
diff --git a/packages/tui/src/util/collapse-tool-output.ts b/packages/tui/src/util/collapse-tool-output.ts
|
|
3199
|
+
index dbdf6e5a1908cae9afe2ceb0c51f5a57f82dcfd3..f8c07cacf0ff15e1f42ca8612f425b929ccf286b 100644
|
|
3200
|
+
--- a/packages/tui/src/util/collapse-tool-output.ts
|
|
3201
|
+
+++ b/packages/tui/src/util/collapse-tool-output.ts
|
|
3202
|
+
@@ -1,3 +1,4 @@
|
|
3203
|
+
+import { chromeEllipsis } from "../ui/glyphs"
|
|
3204
|
+
import { Locale } from "./locale"
|
|
3205
|
+
import { stringWidth } from "./string-width"
|
|
3206
|
+
|
|
3207
|
+
@@ -8,14 +9,11 @@ export function collapseToolOutput(output: string, maxLines: number, maxChars: n
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
const visible = lines.slice(0, maxLines)
|
|
3211
|
+
- if (lines.length > maxLines && visible.length > 0) visible[visible.length - 1] += "…"
|
|
3212
|
+
+ if (lines.length > maxLines && visible.length > 0) visible[visible.length - 1] += chromeEllipsis()
|
|
3213
|
+
const preview = visible.join("\n")
|
|
3214
|
+
if (Array.from(preview).length > maxChars) {
|
|
3215
|
+
return {
|
|
3216
|
+
- output:
|
|
3217
|
+
- Array.from(preview)
|
|
3218
|
+
- .slice(0, Math.max(0, maxChars - 1))
|
|
3219
|
+
- .join("") + "…",
|
|
3220
|
+
+ output: Array.from(preview).slice(0, Math.max(0, maxChars - chromeEllipsis().length)).join("") + chromeEllipsis(),
|
|
3221
|
+
overflow: true,
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
@@ -77,7 +75,7 @@ function collapseShellCommand(input: string, maxLines: number, lineWidth: number
|
|
3225
|
+
const removed = visible.pop()
|
|
3226
|
+
if (removed !== undefined && removed !== "\n") width -= stringWidth(removed)
|
|
3227
|
+
}
|
|
3228
|
+
- return { output: visible.join("") + "…", overflow, lines }
|
|
3229
|
+
+ return { output: visible.join("") + chromeEllipsis(), overflow, lines }
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
function collapseTail(output: string, maxLines: number, maxChars: number) {
|
|
3233
|
+
diff --git a/packages/tui/src/util/error.ts b/packages/tui/src/util/error.ts
|
|
3234
|
+
index 33dff7be582228dc7b6b3c634c3bd387f035881f..96cd6c31ca3a82a7d53a3b147f102e04974999c6 100644
|
|
3235
|
+
--- a/packages/tui/src/util/error.ts
|
|
3236
|
+
+++ b/packages/tui/src/util/error.ts
|
|
3237
|
+
@@ -1,4 +1,5 @@
|
|
3238
|
+
import { isRecord } from "./record"
|
|
3239
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
3240
|
+
|
|
3241
|
+
type ConfigIssue = { message: string; path: string[] }
|
|
3242
|
+
|
|
3243
|
+
@@ -63,7 +64,7 @@ export function cliErrorMessage(input: unknown): string | undefined {
|
|
3244
|
+
: []
|
|
3245
|
+
return [
|
|
3246
|
+
`Configuration is invalid${path && path !== "config" ? ` at ${path}` : ""}` + (message ? `: ${message}` : ""),
|
|
3247
|
+
- ...issues.map((issue) => "↳ " + issue.message + " " + issue.path.join(".")),
|
|
3248
|
+
+ ...issues.map((issue) => `${chromeGlyph("↳")} ` + issue.message + " " + issue.path.join(".")),
|
|
3249
|
+
].join("\n")
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
diff --git a/packages/tui/src/util/locale.ts b/packages/tui/src/util/locale.ts
|
|
3253
|
+
index ac3371eb07959e77e44ab1a7eb3776e8a199642b..4ed4225fbf0147ab9be2eaf0699ccd513b26974d 100644
|
|
3254
|
+
--- a/packages/tui/src/util/locale.ts
|
|
3255
|
+
+++ b/packages/tui/src/util/locale.ts
|
|
3256
|
+
@@ -1,4 +1,5 @@
|
|
3257
|
+
import { stringWidth } from "./string-width"
|
|
3258
|
+
+import { chromeEllipsis, chromeGlyph } from "../ui/glyphs"
|
|
3259
|
+
|
|
3260
|
+
export function titlecase(str: string) {
|
|
3261
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
3262
|
+
@@ -13,7 +14,7 @@ export function datetime(input: number): string {
|
|
3263
|
+
const date = new Date(input)
|
|
3264
|
+
const localTime = time(input)
|
|
3265
|
+
const localDate = date.toLocaleDateString()
|
|
3266
|
+
- return `${localTime} · ${localDate}`
|
|
3267
|
+
+ return `${localTime} ${chromeGlyph("·")} ${localDate}`
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
export function number(num: number): string {
|
|
3271
|
+
@@ -48,9 +49,10 @@ export function duration(input: number) {
|
|
3272
|
+
return `${days}d ${hours}h`
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
-export function truncate(str: string, len: number): string {
|
|
3276
|
+
+export function truncate(str: string, len: number, ellipsis = chromeEllipsis()): string {
|
|
3277
|
+
if (str.length <= len) return str
|
|
3278
|
+
- return str.slice(0, len - 1) + "…"
|
|
3279
|
+
+ if (len <= ellipsis.length) return ellipsis.slice(0, Math.max(0, len))
|
|
3280
|
+
+ return str.slice(0, len - ellipsis.length) + ellipsis
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
3284
|
+
@@ -74,22 +76,24 @@ export function takeWidth(str: string, width: number) {
|
|
3285
|
+
return result.join("")
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
-export function truncateWidth(str: string, width: number): string {
|
|
3289
|
+
+export function truncateWidth(str: string, width: number, ellipsis = chromeEllipsis()): string {
|
|
3290
|
+
if (width <= 0) return ""
|
|
3291
|
+
if (stringWidth(str) <= width) return str
|
|
3292
|
+
- if (width === 1) return "…"
|
|
3293
|
+
- return takeWidth(str, width - 1) + "…"
|
|
3294
|
+
+ const markerWidth = stringWidth(ellipsis)
|
|
3295
|
+
+ if (width <= markerWidth) return takeWidth(ellipsis, width)
|
|
3296
|
+
+ return takeWidth(str, width - markerWidth) + ellipsis
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
-export function truncateLeft(str: string, len: number): string {
|
|
3300
|
+
+export function truncateLeft(str: string, len: number, ellipsis = chromeEllipsis()): string {
|
|
3301
|
+
if (str.length <= len) return str
|
|
3302
|
+
- return "…" + str.slice(-(len - 1))
|
|
3303
|
+
+ if (len <= ellipsis.length) return ellipsis.slice(0, Math.max(0, len))
|
|
3304
|
+
+ return ellipsis + str.slice(-(len - ellipsis.length))
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
-export function truncateMiddle(str: string, maxLength: number = 35): string {
|
|
3308
|
+
+export function truncateMiddle(str: string, maxLength: number = 35, ellipsis = chromeEllipsis()): string {
|
|
3309
|
+
if (str.length <= maxLength) return str
|
|
3310
|
+
|
|
3311
|
+
- const ellipsis = "…"
|
|
3312
|
+
+ if (maxLength <= ellipsis.length) return ellipsis.slice(0, maxLength)
|
|
3313
|
+
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
|
3314
|
+
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
|
3315
|
+
|
|
3316
|
+
diff --git a/packages/tui/src/util/marquee.ts b/packages/tui/src/util/marquee.ts
|
|
3317
|
+
index 8b9428ed4bde4d3da0a00c9daff970564846a4bc..798fa67117f931c425ebfc2cd5f999fc76945e00 100644
|
|
3318
|
+
--- a/packages/tui/src/util/marquee.ts
|
|
3319
|
+
+++ b/packages/tui/src/util/marquee.ts
|
|
3320
|
+
@@ -1,7 +1,8 @@
|
|
3321
|
+
import { Locale } from "./locale"
|
|
3322
|
+
import { stringWidth } from "./string-width"
|
|
3323
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
3324
|
+
|
|
3325
|
+
-const GAP = " · "
|
|
3326
|
+
+const GAP = ` ${chromeGlyph("·")} `
|
|
3327
|
+
|
|
3328
|
+
export type MarqueeTextPart = {
|
|
3329
|
+
value: string
|
|
3330
|
+
@@ -29,7 +30,7 @@ export function marqueeTextParts(value: string, width: number, offset: number):
|
|
3331
|
+
|
|
3332
|
+
const loop = [
|
|
3333
|
+
...Locale.graphemes(value).map((value) => ({ value, separator: false })),
|
|
3334
|
+
- ...Locale.graphemes(GAP).map((value) => ({ value, separator: value === "·" })),
|
|
3335
|
+
+ ...Locale.graphemes(GAP).map((value) => ({ value, separator: value === chromeGlyph("·") })),
|
|
3336
|
+
]
|
|
3337
|
+
const cursor = offset % marqueeCycleWidth(value)
|
|
3338
|
+
const parts = [...loop, ...loop]
|
|
3339
|
+
diff --git a/packages/tui/src/util/permission.ts b/packages/tui/src/util/permission.ts
|
|
3340
|
+
index 09d607ff373ea1075a024b2a14a3aaf522713840..f9bc32c7b380a4bb1e86abfe102c32ba7a7eeeed 100644
|
|
3341
|
+
--- a/packages/tui/src/util/permission.ts
|
|
3342
|
+
+++ b/packages/tui/src/util/permission.ts
|
|
3343
|
+
@@ -1,5 +1,6 @@
|
|
3344
|
+
import { Locale } from "./locale"
|
|
3345
|
+
import { canonicalToolName, finiteNumber, webSearchProviderLabel } from "./tool-display"
|
|
3346
|
+
+import { chromeGlyph } from "../ui/glyphs"
|
|
3347
|
+
|
|
3348
|
+
type Dict = Record<string, unknown>
|
|
3349
|
+
|
|
3350
|
+
@@ -34,7 +35,7 @@ export function permissionPresentation(
|
|
3351
|
+
const first = dict(Array.isArray(metadata.files) ? metadata.files[0] : undefined)
|
|
3352
|
+
const diff = text(first.patch) || text(first.diff) || text(metadata.diff) || undefined
|
|
3353
|
+
return {
|
|
3354
|
+
- icon: "→",
|
|
3355
|
+
+ icon: chromeGlyph("→"),
|
|
3356
|
+
title: `Edit ${formatPath(file)}`,
|
|
3357
|
+
lines: [],
|
|
3358
|
+
diff,
|
|
3359
|
+
@@ -47,7 +48,7 @@ export function permissionPresentation(
|
|
3360
|
+
const value = text(input.path) || resources[0] || ""
|
|
3361
|
+
const title = action === "read" ? "Read" : "List"
|
|
3362
|
+
return {
|
|
3363
|
+
- icon: "→",
|
|
3364
|
+
+ icon: chromeGlyph("→"),
|
|
3365
|
+
title: `${title} ${formatPath(value)}`,
|
|
3366
|
+
lines: value ? [`Path: ${formatPath(value)}`] : [],
|
|
3367
|
+
}
|
|
3368
|
+
@@ -57,7 +58,7 @@ export function permissionPresentation(
|
|
3369
|
+
const pattern = text(input.pattern) || resources[0] || ""
|
|
3370
|
+
const title = action === "glob" ? "Glob" : "Grep"
|
|
3371
|
+
return {
|
|
3372
|
+
- icon: "✱",
|
|
3373
|
+
+ icon: chromeGlyph("✱"),
|
|
3374
|
+
title: `${title} "${pattern}"`,
|
|
3375
|
+
lines: pattern ? [`Pattern: ${pattern}`] : [],
|
|
3376
|
+
}
|
|
3377
|
+
@@ -78,7 +79,7 @@ export function permissionPresentation(
|
|
3378
|
+
return {
|
|
3379
|
+
icon: "#",
|
|
3380
|
+
title: `${Locale.titlecase(agent)} Subagent`,
|
|
3381
|
+
- lines: description ? [`◉ ${description}`] : [],
|
|
3382
|
+
+ lines: description ? [`${chromeGlyph("◉")} ${description}`] : [],
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
@@ -95,7 +96,7 @@ export function permissionPresentation(
|
|
3387
|
+
const query = text(input.query) || text(metadata.query)
|
|
3388
|
+
const title = webSearchProviderLabel(metadata.provider)
|
|
3389
|
+
return {
|
|
3390
|
+
- icon: "◈",
|
|
3391
|
+
+ icon: chromeGlyph("◈"),
|
|
3392
|
+
title: query ? `${title} "${query}"` : title,
|
|
3393
|
+
lines: query ? [`Query: ${query}`] : [],
|
|
3394
|
+
}
|
|
3395
|
+
@@ -108,7 +109,7 @@ export function permissionPresentation(
|
|
3396
|
+
const character = finiteNumber(input.character)
|
|
3397
|
+
const position = line !== undefined && character !== undefined ? `${line}:${character}` : undefined
|
|
3398
|
+
return {
|
|
3399
|
+
- icon: "→",
|
|
3400
|
+
+ icon: chromeGlyph("→"),
|
|
3401
|
+
title: `LSP ${operation}${file ? ` ${formatPath(file)}${position ? `:${position}` : ""}` : ""}`,
|
|
3402
|
+
lines: [
|
|
3403
|
+
...(input.operation ? [`Operation: ${operation}`] : []),
|
|
3404
|
+
@@ -122,7 +123,7 @@ export function permissionPresentation(
|
|
3405
|
+
const raw = text(metadata.parentDir) || text(metadata.filepath) || resources[0] || ""
|
|
3406
|
+
const directory = wildcardDirectory(raw)
|
|
3407
|
+
return {
|
|
3408
|
+
- icon: "←",
|
|
3409
|
+
+ icon: chromeGlyph("←"),
|
|
3410
|
+
title: `Access external directory ${formatPath(directory)}`,
|
|
3411
|
+
lines: resources.map((item) => `- ${item}`),
|
|
3412
|
+
}
|
|
3413
|
+
@@ -130,14 +131,14 @@ export function permissionPresentation(
|
|
3414
|
+
|
|
3415
|
+
if (action === "doom_loop") {
|
|
3416
|
+
return {
|
|
3417
|
+
- icon: "⟳",
|
|
3418
|
+
+ icon: chromeGlyph("⟳"),
|
|
3419
|
+
title: "Continue after repeated failures",
|
|
3420
|
+
lines: ["This keeps the session running despite repeated failures."],
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
return {
|
|
3425
|
+
- icon: "⚙",
|
|
3426
|
+
+ icon: chromeGlyph("⚙"),
|
|
3427
|
+
title: `Call tool ${source.action}`,
|
|
3428
|
+
lines: [`Tool: ${source.action}`],
|
|
3429
|
+
}
|
|
3430
|
+
diff --git a/packages/tui/src/util/session.ts b/packages/tui/src/util/session.ts
|
|
3431
|
+
index c7213e2205edc6d3bf1e068998527722b83fb75a..95f4dd09b2c3cb0e571d3e8ea6b4d673b0ae652a 100644
|
|
3432
|
+
--- a/packages/tui/src/util/session.ts
|
|
3433
|
+
+++ b/packages/tui/src/util/session.ts
|
|
3434
|
+
@@ -1,6 +1,7 @@
|
|
3435
|
+
import type { ModelInfo, SessionMessageInfo } from "@opencode/client"
|
|
3436
|
+
import { lastAssistantWithUsage } from "@opencode/client/solid"
|
|
3437
|
+
import { Locale } from "./locale"
|
|
3438
|
+
+import { asciiGlyphs, chromeGlyph } from "../ui/glyphs"
|
|
3439
|
+
|
|
3440
|
+
type SessionNode = {
|
|
3441
|
+
id: string
|
|
3442
|
+
@@ -32,10 +33,12 @@ export function sessionFamily<T extends SessionNode>(sessions: readonly T[], ses
|
|
3443
|
+
const prefix =
|
|
3444
|
+
ancestors.length === 0
|
|
3445
|
+
? ""
|
|
3446
|
+
- : ancestors
|
|
3447
|
+
+ : asciiGlyphs()
|
|
3448
|
+
+ ? ancestors.slice(1).map((ancestor) => (ancestor ? " " : "| ")).join("") + (last ? "`- " : "|- ")
|
|
3449
|
+
+ : ancestors
|
|
3450
|
+
.slice(1)
|
|
3451
|
+
- .map((ancestor) => (ancestor ? " " : "│ "))
|
|
3452
|
+
- .join("") + (last ? "└─ " : "├─ ")
|
|
3453
|
+
+ .map((ancestor) => (ancestor ? " " : `${chromeGlyph("│")} `))
|
|
3454
|
+
+ .join("") + (last ? `${chromeGlyph("└")}─ ` : `${chromeGlyph("├")}─ `)
|
|
3455
|
+
return [{ session, prefix }, ...walk(session.id, [...ancestors, last])]
|
|
3456
|
+
})
|
|
3457
|
+
}
|
|
3458
|
+
diff --git a/packages/tui/test/glyphs.test.ts b/packages/tui/test/glyphs.test.ts
|
|
3459
|
+
new file mode 100644
|
|
3460
|
+
index 0000000000000000000000000000000000000000..d89b7b9eb18810db774560c3c864a562cb8dc31d
|
|
3461
|
+
--- /dev/null
|
|
3462
|
+
+++ b/packages/tui/test/glyphs.test.ts
|
|
3463
|
+
@@ -0,0 +1,207 @@
|
|
3464
|
+
+import { describe, expect, test } from "bun:test"
|
|
3465
|
+
+
|
|
3466
|
+
+describe("host chrome glyph compatibility", () => {
|
|
3467
|
+
+ test("maps known chrome symbols only in ASCII mode", async () => {
|
|
3468
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3469
|
+
+ const { chromeGlyph, chromeLabel, glyphMode } = await import("../src/ui/glyphs")
|
|
3470
|
+
+ expect(glyphMode()).toBe("ascii")
|
|
3471
|
+
+ expect(chromeGlyph("✓")).toBe("+")
|
|
3472
|
+
+ expect(chromeGlyph("✗")).toBe("x")
|
|
3473
|
+
+ expect(chromeGlyph("◈")).toBe("@")
|
|
3474
|
+
+ expect(chromeGlyph("┃")).toBe("|")
|
|
3475
|
+
+ expect(chromeGlyph("·")).toBe(".")
|
|
3476
|
+
+ expect(chromeGlyph("−")).toBe("-")
|
|
3477
|
+
+ expect(chromeGlyph("—")).toBe("-")
|
|
3478
|
+
+ expect(chromeLabel("Searching…")).toBe("Searching...")
|
|
3479
|
+
+ expect(chromeLabel("literal user title…")).toBe("literal user title…")
|
|
3480
|
+
+ for (const label of [
|
|
3481
|
+
+ "Run a command…", "Ask anything…", "Searching…", "Search worktrees…",
|
|
3482
|
+
+ "Search sessions and projects…", "Connecting …", "Searching sessions…", "Loading sessions…",
|
|
3483
|
+
+ "Writing debug snapshot…", "Checking for updates…", "Loading skills…", "Gathering your stats…",
|
|
3484
|
+
+ "Restarting service…", "Connection lost…", "Receiving code…", "Waiting for code…",
|
|
3485
|
+
+ "Waiting for output…", "Loading worktrees…", "Refreshing sessions and projects…", "Creating worktree…",
|
|
3486
|
+
+ "Loading server information…", "Starting command…", "Waiting for command to finish…",
|
|
3487
|
+
+ "Starting authorization…", "Waiting for authorization…", "Opening link…", "Finishing startup…",
|
|
3488
|
+
+ "Loading plugins…", "Loading image…", "Loading session history…", "Writing command…",
|
|
3489
|
+
+ "Fetching from the web…", "Delegating…", "Preparing edit…", "Forking session…", "Resolving diff…",
|
|
3490
|
+
+ "Loading branches…", "Choose…", "Loading diff…", "processing…", "Working…", "Search sessions…",
|
|
3491
|
+
+ "Loading output…", "Searching content…", "Reading file…", "Preparing write…", "Finding files…",
|
|
3492
|
+
+ "Searching web…", "Asking questions…", "Loading skill…", "Waiting for permission event…", "Waiting…",
|
|
3493
|
+
+ "No matching files…",
|
|
3494
|
+
+ ]) expect(chromeLabel(label)).not.toContain("…")
|
|
3495
|
+
+ expect(chromeGlyph("model text ✓ stays intact")).toBe("model text ✓ stays intact")
|
|
3496
|
+
+ for (const glyph of ["→", "←", "↳", "↻", "⟳", "✱", "◈", "⚙", "✓", "✗", "×", "•", "●", "▪", "■", "◉", "△", "›", "┃", "│", "…", "↑", "↓", "├", "└", "─"])
|
|
3497
|
+
+ expect(chromeGlyph(glyph)).toMatch(/^[\x00-\x7f]+$/)
|
|
3498
|
+
+ })
|
|
3499
|
+
+
|
|
3500
|
+
+ test("selects ASCII spinner and border chrome without changing Unicode defaults", async () => {
|
|
3501
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3502
|
+
+ const { SPINNER_FRAMES } = await import("../src/component/spinner-frames")
|
|
3503
|
+
+ const { SplitBorder, EmptyBorder } = await import("../src/ui/border")
|
|
3504
|
+
+ expect(SPINNER_FRAMES).toEqual(["-", "\\", "|", "/"])
|
|
3505
|
+
+ expect(SplitBorder.customBorderChars).toEqual({
|
|
3506
|
+
+ topLeft: "+", bottomLeft: "+", vertical: "|", topRight: "+", bottomRight: "+",
|
|
3507
|
+
+ horizontal: "-", bottomT: "+", topT: "+", cross: "+", leftT: "+", rightT: "+",
|
|
3508
|
+
+ })
|
|
3509
|
+
+
|
|
3510
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3511
|
+
+ // Border characters are resolved at render time, so explicit Unicode remains intact.
|
|
3512
|
+
+ expect(SplitBorder.customBorderChars).toEqual({ ...EmptyBorder, vertical: "┃" })
|
|
3513
|
+
+ })
|
|
3514
|
+
+
|
|
3515
|
+
+ test("keeps Unicode by default and supports explicit Unicode", async () => {
|
|
3516
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3517
|
+
+ const { chromeGlyph, glyphMode } = await import("../src/ui/glyphs")
|
|
3518
|
+
+ expect(glyphMode()).toBe("unicode")
|
|
3519
|
+
+ expect(chromeGlyph("✓")).toBe("✓")
|
|
3520
|
+
+ const { spinnerFrames } = await import("../src/component/spinner-frames")
|
|
3521
|
+
+ expect(spinnerFrames()).toEqual(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
|
|
3522
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3523
|
+
+ expect(glyphMode()).toBe("unicode")
|
|
3524
|
+
+ })
|
|
3525
|
+
+
|
|
3526
|
+
+ test("auto is Unicode until the launcher supplies a probe result", async () => {
|
|
3527
|
+
+ process.env.CTG_TUI_GLYPHS = "auto"
|
|
3528
|
+
+ const { glyphMode, chromeGlyph } = await import("../src/ui/glyphs")
|
|
3529
|
+
+ expect(glyphMode()).toBe("auto")
|
|
3530
|
+
+ expect(chromeGlyph("✓")).toBe("✓")
|
|
3531
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3532
|
+
+ })
|
|
3533
|
+
+
|
|
3534
|
+
+ test("permission and session-tree chrome follow ASCII mode", async () => {
|
|
3535
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3536
|
+
+ const { permissionPresentation } = await import("../src/util/permission")
|
|
3537
|
+
+ expect(permissionPresentation({ action: "edit", resources: ["file.ts"] }).icon).toBe(">")
|
|
3538
|
+
+ const { sessionFamily } = await import("../src/util/session")
|
|
3539
|
+
+ const family = sessionFamily(
|
|
3540
|
+
+ [
|
|
3541
|
+
+ { id: "root" },
|
|
3542
|
+
+ { id: "child", parentID: "root" },
|
|
3543
|
+
+ { id: "grandchild", parentID: "child" },
|
|
3544
|
+
+ ],
|
|
3545
|
+
+ "grandchild",
|
|
3546
|
+
+ )
|
|
3547
|
+
+ expect(family.map((item) => item.prefix)).toEqual(["", "`- "])
|
|
3548
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3549
|
+
+ expect(
|
|
3550
|
+
+ sessionFamily(
|
|
3551
|
+
+ [{ id: "root" }, { id: "child", parentID: "root" }, { id: "grandchild", parentID: "child" }],
|
|
3552
|
+
+ "grandchild",
|
|
3553
|
+
+ )[1]?.prefix,
|
|
3554
|
+
+ ).toBe("└─ ")
|
|
3555
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3556
|
+
+ })
|
|
3557
|
+
+
|
|
3558
|
+
+ test("full TUI status, truncation, and secondary chrome have ASCII consumers", async () => {
|
|
3559
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3560
|
+
+ const { chromeGlyph, chromeStatusSeparator } = await import("../src/ui/glyphs")
|
|
3561
|
+
+ expect(chromeGlyph("⇆")).toBe("<>")
|
|
3562
|
+
+ expect(chromeGlyph("✕")).toBe("x")
|
|
3563
|
+
+ expect(chromeGlyph("⦿")).toBe("o")
|
|
3564
|
+
+ expect(chromeGlyph("⬖")).toBe("*")
|
|
3565
|
+
+ expect(chromeGlyph("⌕")).toBe("?")
|
|
3566
|
+
+ expect(chromeGlyph("↑")).toBe("^")
|
|
3567
|
+
+ expect(chromeGlyph("↓")).toBe("v")
|
|
3568
|
+
+ expect(chromeGlyph("⎇")).toBe("*")
|
|
3569
|
+
+ expect(chromeStatusSeparator()).toBe("|")
|
|
3570
|
+
+ const { tabUnreadMarker } = await import("../src/component/session-tabs")
|
|
3571
|
+
+ expect(tabUnreadMarker("small-dot")).toBe("*")
|
|
3572
|
+
+ expect(tabUnreadMarker("large-square")).toBe("#")
|
|
3573
|
+
+ const { sessionTabDetail } = await import("../src/context/session-tabs-model")
|
|
3574
|
+
+ expect(sessionTabDetail("project", "feature", "main", true)).toBe("project * feature")
|
|
3575
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3576
|
+
+ expect(sessionTabDetail("project", "feature", "main", true)).toBe("project ⎇ feature")
|
|
3577
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3578
|
+
+
|
|
3579
|
+
+ const { chromeEllipsis, chromeBorderChars } = await import("../src/ui/glyphs")
|
|
3580
|
+
+ expect(chromeEllipsis()).toBe("...")
|
|
3581
|
+
+ expect(chromeBorderChars()).toEqual({
|
|
3582
|
+
+ topLeft: "+", bottomLeft: "+", vertical: "|", topRight: "+", bottomRight: "+",
|
|
3583
|
+
+ horizontal: "-", bottomT: "+", topT: "+", cross: "+", leftT: "+", rightT: "+",
|
|
3584
|
+
+ })
|
|
3585
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3586
|
+
+ })
|
|
3587
|
+
+
|
|
3588
|
+
+ test("ASCII mode changes generated truncation markers, not source text", async () => {
|
|
3589
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3590
|
+
+ const { truncate, truncateWidth, truncateMiddle } = await import("../src/util/locale")
|
|
3591
|
+
+ const { truncateFilePath } = await import("../src/ui/file-path")
|
|
3592
|
+
+ const { collapseToolOutput } = await import("../src/util/collapse-tool-output")
|
|
3593
|
+
+ expect(truncate("αβγδε", 4)).toBe("α...")
|
|
3594
|
+
+ expect(truncateWidth("αβγδε", 4)).toBe("α...")
|
|
3595
|
+
+ expect(truncateMiddle("αβγδεζηθ", 7)).toBe("αβ...ηθ")
|
|
3596
|
+
+ expect(truncateFilePath("/home/alex/projects/éclair.ts", 16)).toContain("...")
|
|
3597
|
+
+ expect(collapseToolOutput("first\nsecond\nthird", 2, 100).output).toBe("first\nsecond...")
|
|
3598
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3599
|
+
+ expect(truncate("αβγδε", 4)).toBe("αβγ…")
|
|
3600
|
+
+ expect(truncateWidth("αβγδε", 4)).toBe("αβγ…")
|
|
3601
|
+
+ expect(truncateFilePath("/home/alex/projects/éclair.ts", 16)).toContain("…")
|
|
3602
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3603
|
+
+ })
|
|
3604
|
+
+
|
|
3605
|
+
+ test("ASCII logo artwork is selected by the mounted full-TUI logo consumer", async () => {
|
|
3606
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3607
|
+
+ const { ASCII_LOGO, asciiGlyphs } = await import("../src/ui/glyphs")
|
|
3608
|
+
+ expect(asciiGlyphs()).toBe(true)
|
|
3609
|
+
+ expect(ASCII_LOGO.left.join("") + ASCII_LOGO.right.join("") + ASCII_LOGO.compact.join("")).toMatch(/^[\x00-\x7f]+$/)
|
|
3610
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3611
|
+
+ expect(asciiGlyphs()).toBe(false)
|
|
3612
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3613
|
+
+ })
|
|
3614
|
+
+
|
|
3615
|
+
+ test("runtime host separators, shell omitted marker, and disclosures are ASCII while values stay intact", async () => {
|
|
3616
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3617
|
+
+ const { chromeGlyph } = await import("../src/ui/glyphs")
|
|
3618
|
+
+ const { shellOutputOmittedLabel } = await import("../src/component/dialog-shell-output")
|
|
3619
|
+
+ const { activityGroupDisclosureIcon } = await import("../src/routes/session/group-view")
|
|
3620
|
+
+ const { promptMetadataLayout } = await import("../src/component/prompt/metadata")
|
|
3621
|
+
+ const { joinPromptFooterStatus } = await import("../src/feature-plugins/prompt/footer")
|
|
3622
|
+
+ const { datetime } = await import("../src/util/locale")
|
|
3623
|
+
+ expect(shellOutputOmittedLabel()).toBe("Earlier output omitted . showing recent output")
|
|
3624
|
+
+ expect(activityGroupDisclosureIcon(true)).toBe("-")
|
|
3625
|
+
+ expect(activityGroupDisclosureIcon(false)).toBe("+")
|
|
3626
|
+
+ const metadata = promptMetadataLayout({
|
|
3627
|
+
+ width: 60, terminalWidth: 80, agent: "agent·name", model: "model−name", provider: "provider", variant: "variant",
|
|
3628
|
+
+ })
|
|
3629
|
+
+ expect(metadata.agent).toBe("agent·name")
|
|
3630
|
+
+ expect(metadata.model).toBe("model−name")
|
|
3631
|
+
+ expect(joinPromptFooterStatus(["shell·label", "user−value"])).toBe("shell·label . user−value")
|
|
3632
|
+
+ expect(datetime(Date.UTC(2024, 0, 1))).toContain(" . ")
|
|
3633
|
+
+ expect("Earlier output omitted · showing recent output".replace("·", chromeGlyph("·"))).toBe("Earlier output omitted . showing recent output")
|
|
3634
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3635
|
+
+ })
|
|
3636
|
+
+
|
|
3637
|
+
+ test("visible prompt metadata and marquee separator map chrome without rewriting dynamic values", async () => {
|
|
3638
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3639
|
+
+ const { promptMetadataLayout } = await import("../src/component/prompt/metadata")
|
|
3640
|
+
+ const { marqueeText } = await import("../src/util/marquee")
|
|
3641
|
+
+ const metadata = promptMetadataLayout({
|
|
3642
|
+
+ width: 60, terminalWidth: 80, agent: "Agent·Name", model: "Model−Name", provider: "Provider", variant: "Variant",
|
|
3643
|
+
+ })
|
|
3644
|
+
+ expect(metadata.agent).toBe("Agent·Name")
|
|
3645
|
+
+ expect(metadata.model).toBe("Model−Name")
|
|
3646
|
+
+ expect(marqueeText("input·text", 30, 1)).toBe("input·text")
|
|
3647
|
+
+ expect(marqueeText("input·text", 5, 7)).toBe("ext .")
|
|
3648
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3649
|
+
+ const unicode = promptMetadataLayout({
|
|
3650
|
+
+ width: 60, terminalWidth: 80, agent: "Agent", model: "Model", provider: "Provider", variant: "Variant",
|
|
3651
|
+
+ })
|
|
3652
|
+
+ expect(unicode.agent).toBe("Agent")
|
|
3653
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3654
|
+
+ })
|
|
3655
|
+
+
|
|
3656
|
+
+ test("session-list relative-path footer executes its truncation branch in both modes", async () => {
|
|
3657
|
+
+ process.env.CTG_TUI_GLYPHS = "ascii"
|
|
3658
|
+
+ const { sessionListRelativePathFooter } = await import("../src/component/dialog-session-list")
|
|
3659
|
+
+ const relative = "../folder·model−name/feature·with-a-long-path-segment-that-needs-truncation.ts"
|
|
3660
|
+
+ const ascii = sessionListRelativePathFooter(relative)
|
|
3661
|
+
+ expect(ascii).toContain("...")
|
|
3662
|
+
+ expect(ascii).toContain("feature·")
|
|
3663
|
+
+ expect(ascii).not.toContain("−")
|
|
3664
|
+
+ process.env.CTG_TUI_GLYPHS = "unicode"
|
|
3665
|
+
+ const unicode = sessionListRelativePathFooter(relative)
|
|
3666
|
+
+ expect(unicode).toContain("…")
|
|
3667
|
+
+ expect(unicode).toContain("feature·")
|
|
3668
|
+
+ delete process.env.CTG_TUI_GLYPHS
|
|
3669
|
+
+ })
|
|
3670
|
+
+})
|