clauderipple 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +31 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +43 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +48 -40
- package/dist/cli/src/settings.js +2 -1
- package/dist/mod/hooks/i18n.ts +252 -6
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +238 -32
- package/dist/mod/hooks/session.ts +98 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +858 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +116 -3
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +375 -27
- package/dist/mod/types/index.d.ts +68 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +295 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +227 -4
- package/dist/router/src/config.js +30 -0
- package/dist/router/src/index.js +39 -0
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/presets.js +3 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/chatgpt/index.js +41 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- package/dist/router/src/providers/google/index.js +26 -0
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/usage.js +257 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +260 -27
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/reasoning.js +157 -0
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/thinking.js +20 -9
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -59
- package/dist/ui/i18n.js +184 -0
- package/dist/ui/index.html +24 -5
- package/dist/ui/style.css +39 -0
- package/docs/ARCHITECTURE.md +164 -12
- package/package.json +7 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The skin (2026-10-14): which pictures the desktop draws with and what colours its host Text takes.
|
|
2
|
+
// The deep (art.ts) is the first look: dark water on either theme, 리플이's glow, drawn controls in
|
|
3
|
+
// glass. Flat (flat.ts) is the app's own surfaces: a hairline card that follows the theme, the
|
|
4
|
+
// host's own ink, the accent only where something is on. Those two share one layout and change
|
|
5
|
+
// paint alone: every size is art.ts's and every cell count the same under both, so band.tsx's
|
|
6
|
+
// arithmetic and the blank presses laid over the pictures (kit.tsx) hold whatever the skin. The
|
|
7
|
+
// terminal draws text and knows nothing of skins.
|
|
8
|
+
|
|
9
|
+
import type { Skin } from '../../types'
|
|
10
|
+
import * as deep from './art'
|
|
11
|
+
import type { Glyph, Icon, PillLook, Sea, TileArt, Tone } from './art'
|
|
12
|
+
import { flatPictures } from './flat'
|
|
13
|
+
import { rippleSource } from './parts'
|
|
14
|
+
import type { Face } from './ripple'
|
|
15
|
+
import { textTone } from './theme'
|
|
16
|
+
import type { Appearance } from './theme'
|
|
17
|
+
import type { Frame } from './view'
|
|
18
|
+
|
|
19
|
+
/** Props a host Text takes for a role on a capsule: a colour of ours, or the host's own ink. */
|
|
20
|
+
export type Word = { color?: string; dimColor?: boolean }
|
|
21
|
+
|
|
22
|
+
/** Props a Box takes: a capsule, a tile, a control lit under the pointer. */
|
|
23
|
+
export type Surface = { borderStyle?: string; borderColor?: string; backgroundColor?: string; hover?: { borderColor?: string; backgroundColor?: string } }
|
|
24
|
+
|
|
25
|
+
/** The pictures a skin paints, each at art.ts's size. */
|
|
26
|
+
export type Paint = {
|
|
27
|
+
/** Every capsule's backdrop, `width` × `height` px; '' when the skin paints none and the Box's own colour shows. */
|
|
28
|
+
sea: (width: number, height: number, sea: Sea) => string
|
|
29
|
+
plate: (width: number, height: number) => string
|
|
30
|
+
switch: (on: boolean, busy?: boolean) => string
|
|
31
|
+
word: (label: string, on: boolean, busy?: boolean) => { source: string; cells: number }
|
|
32
|
+
extraHover: (widthPx: number) => string
|
|
33
|
+
pill: (label: string, look: PillLook, tone?: Tone, glyph?: Glyph) => { source: string; cells: number }
|
|
34
|
+
segmented: (labels: readonly string[], cells: readonly number[], chosen: number, tone?: Tone, colors?: readonly string[]) => string
|
|
35
|
+
slider: (count: number, cellsPerStop: number, chosen: number) => string
|
|
36
|
+
well: (width: number, height: number) => string
|
|
37
|
+
close: (lit?: boolean) => string
|
|
38
|
+
meter: (lit: number, look: 'auto' | 'manual' | 'quiet' | 'deciding') => string
|
|
39
|
+
ring: (percent: number, tone: Tone | 'ink') => string
|
|
40
|
+
clock: (share: number, tone: Tone | 'soft') => string
|
|
41
|
+
gauge: (kind: string) => string
|
|
42
|
+
icon: (icon: Icon, on: boolean, tone?: Tone) => string
|
|
43
|
+
agents: (count: number, on: boolean) => string
|
|
44
|
+
rule: () => string
|
|
45
|
+
tile: (art: TileArt) => string
|
|
46
|
+
beads: (step: number, total: number) => { source: string; width: number }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Pictures = Paint & {
|
|
50
|
+
/** 리플이's document at `size` px, in the skin's palette (parts.tsx `rippleSource`). */
|
|
51
|
+
mascot: (face: Face, size: number, percent?: number, dim?: boolean) => string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Look = {
|
|
55
|
+
skin: Skin
|
|
56
|
+
/** Host Text on a capsule: bright for values, soft for names, quiet for asides. */
|
|
57
|
+
ink: Word
|
|
58
|
+
sub: Word
|
|
59
|
+
mute: Word
|
|
60
|
+
/** The text colour of a tone: a card's title in its light, a figure gone amber. */
|
|
61
|
+
tone: (tone: Tone) => string
|
|
62
|
+
capsule: Surface
|
|
63
|
+
tile: Surface
|
|
64
|
+
/** A control under the pointer (kit.tsx `lit`): its hover alone. */
|
|
65
|
+
lit: Surface
|
|
66
|
+
art: Pictures
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEEP_PAINT: Paint = {
|
|
70
|
+
sea: deep.seaArt, plate: deep.plateArt, switch: deep.switchArt, word: deep.wordArt, extraHover: deep.extraHoverArt,
|
|
71
|
+
pill: deep.pillArt, segmented: deep.segmentedArt, slider: deep.sliderArt, well: deep.wellArt, close: deep.closeArt,
|
|
72
|
+
meter: deep.meterArt, ring: deep.ringArt, clock: deep.clockArt, gauge: deep.gaugeArt, icon: deep.iconArt,
|
|
73
|
+
agents: deep.agentsArt, rule: deep.ruleArt, tile: deep.tileArt, beads: deep.beadsArt,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The deep: the capsule's edge a warm grey that reads on a light page and a dark one alike, lights white on the water. */
|
|
77
|
+
const DEEP: Look = {
|
|
78
|
+
skin: 'deep',
|
|
79
|
+
ink: { color: deep.INK },
|
|
80
|
+
sub: { color: deep.SUB },
|
|
81
|
+
mute: { color: deep.MUTE },
|
|
82
|
+
tone: tone => deep.TONE[tone].text,
|
|
83
|
+
capsule: { borderStyle: 'round', borderColor: 'rgba(128,124,116,0.55)' },
|
|
84
|
+
tile: { borderStyle: 'round', borderColor: 'rgba(255,255,255,0.1)', backgroundColor: 'rgba(255,255,255,0.045)', hover: { borderColor: 'rgba(255,255,255,0.26)', backgroundColor: 'rgba(255,255,255,0.075)' } },
|
|
85
|
+
lit: { borderStyle: 'round', borderColor: 'rgba(0,0,0,0)', hover: { backgroundColor: 'rgba(255,255,255,0.09)', borderColor: 'rgba(255,255,255,0.16)' } },
|
|
86
|
+
// 리플이 drawn for the deep: the dark palette, light ink and lavender tone, whatever the theme.
|
|
87
|
+
art: { ...DEEP_PAINT, mascot: (face, size, percent, dim) => rippleSource(face, size, 'dark', percent, dim) },
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Flat: the capsule painted in the app's own surface (flat.ts `sea`, white on a light app as its
|
|
92
|
+
* prompt box is), edged in a grey that sits on either theme, the host's own ink for values and the
|
|
93
|
+
* app's quiet text colour for names and asides, and the pictures in the theme's palette. The
|
|
94
|
+
* host's `dimColor` was too faint to read on a light page (2026-10-16). The engine takes a theme
|
|
95
|
+
* key, a name or hex here (not the app's CSS variables) and does not say which theme is on, so
|
|
96
|
+
* with appearance auto the quiet words are a mid tone between the app's two. One per appearance.
|
|
97
|
+
*/
|
|
98
|
+
const FLATS = new Map<Appearance, Look>()
|
|
99
|
+
|
|
100
|
+
/** The app's quiet text (text-400, read from its stylesheet 2026-10-16) per theme, and between them. */
|
|
101
|
+
const QUIET: Record<Appearance, string> = { light: '#73726c', dark: '#9c9a92', auto: '#86847d' }
|
|
102
|
+
|
|
103
|
+
function flat(appearance: Appearance): Look {
|
|
104
|
+
let look = FLATS.get(appearance)
|
|
105
|
+
if (look) return look
|
|
106
|
+
const text = textTone(appearance)
|
|
107
|
+
look = {
|
|
108
|
+
skin: 'flat',
|
|
109
|
+
ink: {},
|
|
110
|
+
sub: { color: QUIET[appearance] },
|
|
111
|
+
mute: { color: QUIET[appearance] },
|
|
112
|
+
// One accent: the deep's decorative tones (the judge's teal, the mode's amber) are the accent
|
|
113
|
+
// here, and amber, red and green stay what they mean.
|
|
114
|
+
tone: tone => (tone === 'amber' || tone === 'ember' ? text.warn : tone === 'red' ? text.bad : tone === 'green' ? text.ok : text.accent),
|
|
115
|
+
capsule: { borderStyle: 'round', borderColor: 'rgba(128,124,116,0.3)' },
|
|
116
|
+
tile: { borderStyle: 'round', borderColor: 'rgba(128,124,116,0.22)', backgroundColor: 'rgba(128,124,116,0.04)', hover: { borderColor: 'rgba(128,124,116,0.4)', backgroundColor: 'rgba(128,124,116,0.08)' } },
|
|
117
|
+
lit: { borderStyle: 'round', borderColor: 'rgba(0,0,0,0)', hover: { backgroundColor: 'rgba(128,124,116,0.12)', borderColor: 'rgba(0,0,0,0)' } },
|
|
118
|
+
art: { ...flatPictures(appearance), mascot: (face, size, percent, dim) => rippleSource(face, size, appearance, percent, dim) },
|
|
119
|
+
}
|
|
120
|
+
FLATS.set(appearance, look)
|
|
121
|
+
return look
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The look a frame draws in: flat's own surfaces for flat, the deep for everything else.
|
|
126
|
+
*/
|
|
127
|
+
export function lookOf(frame: Frame): Look {
|
|
128
|
+
return frame.skin === 'flat' ? flat(frame.appearance) : DEEP
|
|
129
|
+
}
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
import { t } from '../i18n'
|
|
11
11
|
import type { RenderNode } from 'claude-code'
|
|
12
12
|
|
|
13
|
-
import type { Settings } from '../../types'
|
|
14
|
-
import {
|
|
13
|
+
import type { Settings, Skin } from '../../types'
|
|
14
|
+
import { SKINS } from '../settings'
|
|
15
|
+
import { CELL_PX, TILE_PX } from './art'
|
|
15
16
|
import type { TileArt } from './art'
|
|
16
17
|
import { capsule, pill, pic, ripple, segmented, slider, toggle, well } from './kit'
|
|
18
|
+
import { lookOf } from './look'
|
|
19
|
+
import type { Word } from './look'
|
|
17
20
|
import { action, actions, canDraw, dismiss, segmented as textSegmented, speak, toggle as textToggle } from './parts'
|
|
18
21
|
import type { Els } from './parts'
|
|
19
22
|
import { chOf, displayWidth, textTone } from './theme'
|
|
@@ -21,7 +24,7 @@ import type { Frame, SettingsActions, SettingsView } from './view'
|
|
|
21
24
|
import { isRich } from './view'
|
|
22
25
|
|
|
23
26
|
/** Every name a row of quick settings or setup shows: the name column is the widest of them. */
|
|
24
|
-
const ROW_NAMES = ['settings.effortAuto', 'settings.lean', 'settings.judge', 'settings.key', 'settings.keepWarm', 'settings.workers', 'settings.mode', 'band.effort', 'band.cache',
|
|
27
|
+
const ROW_NAMES = ['settings.effortAuto', 'settings.lean', 'settings.judge', 'settings.key', 'settings.keepWarm', 'settings.workers', 'settings.mode', 'settings.skin', 'band.effort', 'band.cache',
|
|
25
28
|
'workers.rung.light', 'workers.rung.standard', 'workers.rung.hard', 'workers.rung.plan'] as const
|
|
26
29
|
|
|
27
30
|
/** The name column's width in cells: snug to this language's names, so controls sit close to them. */
|
|
@@ -73,7 +76,7 @@ export function effortRows(el: Els, frame: Frame, d: Settings, a: SettingsAction
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
/** The Jev key field and its test, when TypeSafe's Jev judges and no key is known. */
|
|
76
|
-
function needsKey(view: SettingsView): boolean {
|
|
79
|
+
export function needsKey(view: SettingsView): boolean {
|
|
77
80
|
return view.settings.judge === 'jev' && view.settings.jevSource === 'typesafe' && !['env', 'file', 'pasted'].includes(view.jev.source ?? '')
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -151,7 +154,7 @@ export function keepWarmRow(el: Els, frame: Frame, d: Settings, a: SettingsActio
|
|
|
151
154
|
return row(el, frame, 'warm', t(frame.lang, 'settings.keepWarm'), textToggle(el, frame, 'set-warm', d.keepWarm ? t(frame.lang, 'band.on') : t(frame.lang, 'band.off'), d.keepWarm, () => a.patch({ keepWarm: !d.keepWarm })), t(frame.lang, 'settings.keepWarmHint'))
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
function modeItems(frame: Frame, d: Settings): { value: string; label: string }[] {
|
|
157
|
+
export function modeItems(frame: Frame, d: Settings): { value: string; label: string }[] {
|
|
155
158
|
return [{ value: '', label: t(frame.lang, 'settings.modeNone') }, ...d.modes.map(m => ({ value: m.id, label: m.name }))]
|
|
156
159
|
}
|
|
157
160
|
|
|
@@ -165,11 +168,6 @@ function judgeName(frame: Frame, judge: Settings['judge']): string {
|
|
|
165
168
|
// name in bright ink with its switch at the right when it has one, one line on what it does, then
|
|
166
169
|
// its control. The pictures replace the old line icons, which said nothing at 15 px.
|
|
167
170
|
|
|
168
|
-
/** A tile's edge and glass: light on the water, brighter under the pointer. */
|
|
169
|
-
const TILE_EDGE = 'rgba(255,255,255,0.1)'
|
|
170
|
-
const TILE_EDGE_HOT = 'rgba(255,255,255,0.26)'
|
|
171
|
-
const TILE_GLASS = 'rgba(255,255,255,0.045)'
|
|
172
|
-
const TILE_GLASS_HOT = 'rgba(255,255,255,0.075)'
|
|
173
171
|
/** The least a tile takes, in cells: two side by side from about 84 cells, one a row below. */
|
|
174
172
|
const TILE_MIN = 40
|
|
175
173
|
|
|
@@ -186,19 +184,21 @@ function tileInner(frame: Frame): number {
|
|
|
186
184
|
|
|
187
185
|
type Tile = { id: string; art: TileArt; title: string; head?: RenderNode; line: string; body: RenderNode[] }
|
|
188
186
|
|
|
189
|
-
|
|
187
|
+
/** A tile in the skin's edge and glass (look.ts `tile`). */
|
|
188
|
+
function tile(el: Els, frame: Frame, x: Tile): RenderNode {
|
|
190
189
|
const { Box, Text } = el
|
|
190
|
+
const k = lookOf(frame)
|
|
191
191
|
return (
|
|
192
|
-
<Box key={'tile-' + x.id} position="relative" flexDirection="row" alignItems="flex-start" columnGap={2} flexGrow={1} width={0} minWidth={TILE_MIN} paddingX={1.5} paddingY={1}
|
|
193
|
-
{pic(el, 'tile-art-' + x.id,
|
|
192
|
+
<Box key={'tile-' + x.id} position="relative" flexDirection="row" alignItems="flex-start" columnGap={2} flexGrow={1} width={0} minWidth={TILE_MIN} paddingX={1.5} paddingY={1} {...k.tile}>
|
|
193
|
+
{pic(el, 'tile-art-' + x.id, k.art.tile(x.art), x.title, TILE_PX, TILE_PX)}
|
|
194
194
|
<Box key={'tile-body-' + x.id} flexDirection="column" rowGap={1} flexGrow={1} flexShrink={1} minWidth={0}>
|
|
195
195
|
<Box flexDirection="row" alignItems="center" columnGap={1} width="100%" minWidth={0}>
|
|
196
196
|
<Box flexGrow={1} flexShrink={1} minWidth={0}>
|
|
197
|
-
<Text bold
|
|
197
|
+
<Text bold {...k.ink} wrap="wrap">{x.title}</Text>
|
|
198
198
|
</Box>
|
|
199
199
|
{x.head ?? null}
|
|
200
200
|
</Box>
|
|
201
|
-
<Text
|
|
201
|
+
<Text {...k.sub} wrap="wrap">{x.line}</Text>
|
|
202
202
|
{x.body}
|
|
203
203
|
</Box>
|
|
204
204
|
</Box>
|
|
@@ -206,9 +206,9 @@ function tile(el: Els, x: Tile): RenderNode {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
/** A quiet line under a control. */
|
|
209
|
-
function note(el: Els, key: string, text: string,
|
|
209
|
+
function note(el: Els, frame: Frame, key: string, text: string, word?: Word): RenderNode {
|
|
210
210
|
const { Text } = el
|
|
211
|
-
return <Text key={key}
|
|
211
|
+
return <Text key={key} {...(word ?? lookOf(frame).mute)} wrap="wrap">{text}</Text>
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/** Automatic effort: its switch, and while on the lean between cheaper and smarter. */
|
|
@@ -216,21 +216,22 @@ export function effortTile(el: Els, frame: Frame, view: SettingsView, a: Setting
|
|
|
216
216
|
const { Box, Text } = el
|
|
217
217
|
const L = frame.lang
|
|
218
218
|
const d = view.settings
|
|
219
|
+
const k = lookOf(frame)
|
|
219
220
|
const body = d.effortAuto
|
|
220
221
|
? [
|
|
221
222
|
<Box key="lean" flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap">
|
|
222
|
-
<Text
|
|
223
|
-
{slider(el, 'bias', BIAS, d.bias, bias => a.patch({ bias }), 3)}
|
|
224
|
-
<Text
|
|
223
|
+
<Text {...k.mute}>{t(L, 'settings.cheaper')}</Text>
|
|
224
|
+
{slider(el, frame, 'bias', BIAS, d.bias, bias => a.patch({ bias }), 3)}
|
|
225
|
+
<Text {...k.mute}>{t(L, 'settings.smarter')}</Text>
|
|
225
226
|
</Box>,
|
|
226
|
-
note(el, 'lean-note', t(L, 'settings.leanHint')),
|
|
227
|
+
note(el, frame, 'lean-note', t(L, 'settings.leanHint')),
|
|
227
228
|
]
|
|
228
|
-
: [note(el, 'auto-off', t(L, 'settings.autoOffNote'))]
|
|
229
|
-
return tile(el, {
|
|
229
|
+
: [note(el, frame, 'auto-off', t(L, 'settings.autoOffNote'))]
|
|
230
|
+
return tile(el, frame, {
|
|
230
231
|
id: 'effort',
|
|
231
232
|
art: { kind: 'effort', on: d.effortAuto },
|
|
232
233
|
title: t(L, 'settings.effortAuto'),
|
|
233
|
-
head: toggle(el, 'set-auto', d.effortAuto, t(L, d.effortAuto ? 'band.on' : 'band.off'), () => a.patch({ effortAuto: !d.effortAuto })),
|
|
234
|
+
head: toggle(el, frame, 'set-auto', d.effortAuto, t(L, d.effortAuto ? 'band.on' : 'band.off'), () => a.patch({ effortAuto: !d.effortAuto })),
|
|
234
235
|
line: t(L, 'settings.autoLine'),
|
|
235
236
|
body,
|
|
236
237
|
})
|
|
@@ -241,34 +242,35 @@ export function judgeTile(el: Els, frame: Frame, view: SettingsView, a: Settings
|
|
|
241
242
|
const { Box, Text } = el
|
|
242
243
|
const L = frame.lang
|
|
243
244
|
const d = view.settings
|
|
245
|
+
const k = lookOf(frame)
|
|
244
246
|
const body: RenderNode[] = [
|
|
245
|
-
segmented(el, 'judge', [
|
|
247
|
+
segmented(el, frame, 'judge', [
|
|
246
248
|
{ value: 'haiku', label: t(L, 'settings.haiku') },
|
|
247
249
|
{ value: 'jev', label: t(L, 'settings.jev') },
|
|
248
250
|
{ value: 'model', label: t(L, 'settings.routerModel') },
|
|
249
251
|
] as const, d.judge, judge => a.patch({ judge }), 'teal'),
|
|
250
252
|
]
|
|
251
|
-
if (d.judge === 'model' && d.judgeModel) body.push(note(el, 'judge-model-name', t(L, 'settings.routerModel') + ' · ' + d.judgeModel))
|
|
253
|
+
if (d.judge === 'model' && d.judgeModel) body.push(note(el, frame, 'judge-model-name', t(L, 'settings.routerModel') + ' · ' + d.judgeModel))
|
|
252
254
|
if (needsKey(view)) {
|
|
253
255
|
const jev = view.jev
|
|
254
256
|
const placeholder = t(L, 'settings.keyPaste')
|
|
255
257
|
const field = 'Input' in el
|
|
256
|
-
? well(el, 'jev-key', tileInner(frame), <el.Input key="jev-key" placeholder={placeholder} value="" submitLabel={t(L, 'settings.saveKey')} onSubmit={(key: string) => a.jevKey(key)} />, placeholder)
|
|
257
|
-
: <Text
|
|
258
|
+
? well(el, frame, 'jev-key', tileInner(frame), <el.Input key="jev-key" placeholder={placeholder} value="" submitLabel={t(L, 'settings.saveKey')} onSubmit={(key: string) => a.jevKey(key)} />, placeholder)
|
|
259
|
+
: <Text {...k.mute}>{placeholder}</Text>
|
|
258
260
|
const result =
|
|
259
|
-
jev.test.state === 'testing' ? <Text
|
|
260
|
-
: jev.test.state === 'ok' ? <Text color={
|
|
261
|
-
: jev.test.state === 'fail' ? <Text color={
|
|
261
|
+
jev.test.state === 'testing' ? <Text {...k.sub}>{t(L, 'settings.testing')}</Text>
|
|
262
|
+
: jev.test.state === 'ok' ? <Text color={k.tone('green')} wrap="wrap">{'✓ ' + (jev.test.message ?? t(L, 'settings.keyWorks'))}</Text>
|
|
263
|
+
: jev.test.state === 'fail' ? <Text color={k.tone('red')} wrap="wrap">{'✕ ' + (jev.test.message ?? t(L, 'settings.keyRejected'))}</Text>
|
|
262
264
|
: null
|
|
263
265
|
body.push(
|
|
264
266
|
field,
|
|
265
267
|
<Box key="jev-row" flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={1} rowGap={1}>
|
|
266
|
-
{pill(el, 'jev-test', t(L, 'settings.test'), 'ghost', a.testJev, 'teal')}
|
|
268
|
+
{pill(el, frame, 'jev-test', t(L, 'settings.test'), 'ghost', a.testJev, 'teal')}
|
|
267
269
|
{result}
|
|
268
270
|
</Box>,
|
|
269
271
|
)
|
|
270
272
|
}
|
|
271
|
-
return tile(el, { id: 'judge', art: { kind: 'judge', judge: d.judge }, title: t(L, 'settings.judge'), line: t(L, 'settings.judgeLine'), body })
|
|
273
|
+
return tile(el, frame, { id: 'judge', art: { kind: 'judge', judge: d.judge }, title: t(L, 'settings.judge'), line: t(L, 'settings.judgeLine'), body })
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
/** The worker set that is on: Base or a named mode. */
|
|
@@ -278,22 +280,43 @@ export function modeTile(el: Els, frame: Frame, view: SettingsView, a: SettingsA
|
|
|
278
280
|
const items = modeItems(frame, d)
|
|
279
281
|
const current = d.activeMode ?? ''
|
|
280
282
|
const control = items.length <= 5 || !('Select' in el)
|
|
281
|
-
? segmented(el, 'mode', items, current, id => a.setMode?.(id), 'amber')
|
|
283
|
+
? segmented(el, frame, 'mode', items, current, id => a.setMode?.(id), 'amber')
|
|
282
284
|
: <el.Select key="mode" options={items} value={current} onSelect={(value: string) => a.setMode?.(value)} />
|
|
283
|
-
return tile(el, { id: 'mode', art: { kind: 'mode', on: current !== '' }, title: t(L, 'settings.mode'), line: t(L, 'settings.modeLine'), body: [control] })
|
|
285
|
+
return tile(el, frame, { id: 'mode', art: { kind: 'mode', on: current !== '' }, title: t(L, 'settings.mode'), line: t(L, 'settings.modeLine'), body: [control] })
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
/** Keep warm: its switch, and what each ping costs. */
|
|
287
289
|
export function warmTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
288
290
|
const L = frame.lang
|
|
289
291
|
const d = view.settings
|
|
290
|
-
return tile(el, {
|
|
292
|
+
return tile(el, frame, {
|
|
291
293
|
id: 'warm',
|
|
292
294
|
art: { kind: 'warm', on: d.keepWarm },
|
|
293
295
|
title: t(L, 'settings.keepWarm'),
|
|
294
|
-
head: toggle(el, 'set-warm', d.keepWarm, t(L, d.keepWarm ? 'band.on' : 'band.off'), () => a.patch({ keepWarm: !d.keepWarm })),
|
|
296
|
+
head: toggle(el, frame, 'set-warm', d.keepWarm, t(L, d.keepWarm ? 'band.on' : 'band.off'), () => a.patch({ keepWarm: !d.keepWarm })),
|
|
295
297
|
line: t(L, 'settings.warmLine'),
|
|
296
|
-
body: [note(el, 'warm-cost', t(L, 'settings.warmCostShort'))],
|
|
298
|
+
body: [note(el, frame, 'warm-cost', t(L, 'settings.warmCostShort'))],
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The two skins by name, in the order they are offered. */
|
|
303
|
+
function skinOptions(frame: Frame): { value: Skin; label: string }[] {
|
|
304
|
+
return SKINS.map(skin => ({ value: skin, label: t(frame.lang, skin === 'deep' ? 'settings.skinDeep' : 'settings.skinFlat') }))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The skin: how the desktop draws the band and the cards (look.ts), picked here or on the
|
|
309
|
+
* dashboard's Look card. Only the desktop shows it: the terminal draws text whatever it is.
|
|
310
|
+
*/
|
|
311
|
+
export function skinTile(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
312
|
+
const L = frame.lang
|
|
313
|
+
const d = view.settings
|
|
314
|
+
return tile(el, frame, {
|
|
315
|
+
id: 'skin',
|
|
316
|
+
art: { kind: 'skin', skin: d.skin },
|
|
317
|
+
title: t(L, 'settings.skin'),
|
|
318
|
+
line: t(L, 'settings.skinLine'),
|
|
319
|
+
body: [segmented(el, frame, 'skin', skinOptions(frame), d.skin, skin => a.patch({ skin }))],
|
|
297
320
|
})
|
|
298
321
|
}
|
|
299
322
|
|
|
@@ -304,29 +327,30 @@ export function settingsSay(frame: Frame, d: Settings): string {
|
|
|
304
327
|
return d.keepWarm ? effort + ' ' + t(L, 'settings.sayWarm') : effort
|
|
305
328
|
}
|
|
306
329
|
|
|
307
|
-
/** Rows the quick settings take on the desktop, about: for the sea's height. */
|
|
330
|
+
/** Rows the quick settings take on the desktop, about: for the sea's height (five tiles, three rows of two or five of one). */
|
|
308
331
|
function panelRows(frame: Frame, view: SettingsView): number {
|
|
309
|
-
return (tilesAcross(frame) === 2 ?
|
|
332
|
+
return (tilesAcross(frame) === 2 ? 30 : 45) + (needsKey(view) ? 4 : 0)
|
|
310
333
|
}
|
|
311
334
|
|
|
312
335
|
function settingsDrawn(el: Els, frame: Frame, view: SettingsView, a: SettingsActions): RenderNode {
|
|
313
336
|
const { Box, Text } = el
|
|
314
337
|
const L = frame.lang
|
|
315
338
|
const d = view.settings
|
|
339
|
+
const k = lookOf(frame)
|
|
316
340
|
const face = 48
|
|
317
341
|
const head = (
|
|
318
342
|
<Box key="settings-head" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
319
|
-
{ripple(el, 'settings', d.effortAuto ? 'calm' : 'drowsy', t(L, 'band.brand'), face)}
|
|
343
|
+
{ripple(el, frame, 'settings', d.effortAuto ? 'calm' : 'drowsy', t(L, 'band.brand'), face)}
|
|
320
344
|
<Box key="settings-say" flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
321
345
|
<Box flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap">
|
|
322
|
-
<Text bold
|
|
323
|
-
<Text
|
|
346
|
+
<Text bold {...k.ink}>{t(L, 'settings.quick')}</Text>
|
|
347
|
+
<Text {...k.mute}>{t(L, 'band.brand')}</Text>
|
|
324
348
|
</Box>
|
|
325
|
-
<Text
|
|
349
|
+
<Text {...k.sub} wrap="wrap">{settingsSay(frame, d)}</Text>
|
|
326
350
|
</Box>
|
|
327
351
|
<Box key="settings-exits" flexDirection="row" alignItems="center" columnGap={1} flexShrink={0}>
|
|
328
|
-
{pill(el, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open')}
|
|
329
|
-
{pill(el, 'close', t(L, 'settings.close'), 'ghost', a.close, 'violet', 'close')}
|
|
352
|
+
{pill(el, frame, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open')}
|
|
353
|
+
{pill(el, frame, 'close', t(L, 'settings.close'), 'ghost', a.close, 'violet', 'close')}
|
|
330
354
|
</Box>
|
|
331
355
|
</Box>
|
|
332
356
|
)
|
|
@@ -336,6 +360,7 @@ function settingsDrawn(el: Els, frame: Frame, view: SettingsView, a: SettingsAct
|
|
|
336
360
|
{judgeTile(el, frame, view, a)}
|
|
337
361
|
{modeTile(el, frame, view, a)}
|
|
338
362
|
{warmTile(el, frame, view, a)}
|
|
363
|
+
{skinTile(el, frame, view, a)}
|
|
339
364
|
</Box>
|
|
340
365
|
)
|
|
341
366
|
return capsule(el, frame, {
|
|
@@ -10,7 +10,8 @@ import { t } from '../i18n'
|
|
|
10
10
|
import type { RenderNode } from 'claude-code'
|
|
11
11
|
import { RUNGS } from '../settings'
|
|
12
12
|
|
|
13
|
-
import { CELL_PX
|
|
13
|
+
import { CELL_PX } from './art'
|
|
14
|
+
import { lookOf } from './look'
|
|
14
15
|
import { capsule, pic, pill, ripple } from './kit'
|
|
15
16
|
import { GREET_SIZE, action, actions, canDraw, speak } from './parts'
|
|
16
17
|
import type { Els } from './parts'
|
|
@@ -19,14 +20,14 @@ import { chOf } from './theme'
|
|
|
19
20
|
import type { Frame, SettingsActions, SettingsView, SetupActions, SetupView } from './view'
|
|
20
21
|
import { targetLabel } from './view'
|
|
21
22
|
|
|
22
|
-
const STEPS = [
|
|
23
|
+
export const STEPS = [
|
|
23
24
|
{ title: 'setup.judgeTitle', blurb: 'setup.judgeBlurb' },
|
|
24
25
|
{ title: 'setup.workersTitle', blurb: 'setup.workersNote' },
|
|
25
26
|
{ title: 'setup.cacheTitle', blurb: 'setup.cacheBlurb' },
|
|
26
27
|
] as const
|
|
27
28
|
|
|
28
29
|
/** `Sol · Astra · …`: the four workers' models, lowest first. */
|
|
29
|
-
function workersSummary(lang: Frame['lang'], d: SetupView['draft']): string {
|
|
30
|
+
export function workersSummary(lang: Frame['lang'], d: SetupView['draft']): string {
|
|
30
31
|
const general = d.rules.general
|
|
31
32
|
return RUNGS.map(rung => general.rungs[rung]?.map(step => targetLabel(step.target, lang)).join(' → ') || '—').join(' · ')
|
|
32
33
|
}
|
|
@@ -73,7 +74,8 @@ export function renderSetup(el: Els, frame: Frame, view: SetupView, a: SetupActi
|
|
|
73
74
|
// The desktop: a capsule of the deep, the step's control the quick settings' own tile (who
|
|
74
75
|
// judges, keep warm), what was chosen when done, Next lit and the rest ghost buttons.
|
|
75
76
|
const L = frame.lang
|
|
76
|
-
const
|
|
77
|
+
const k = lookOf(frame)
|
|
78
|
+
const beads = k.art.beads(view.step, 3)
|
|
77
79
|
const summary = done
|
|
78
80
|
? [
|
|
79
81
|
[t(L, 'band.effort'), t(L, 'setup.effortSummary', { state: t(L, d.effortAuto ? 'band.on' : 'band.off').toLowerCase(), judge: d.judge === 'haiku' ? t(L, 'settings.haiku') : d.judge === 'jev' ? t(L, 'settings.jev') : d.judgeModel })],
|
|
@@ -87,8 +89,8 @@ export function renderSetup(el: Els, frame: Frame, view: SetupView, a: SetupActi
|
|
|
87
89
|
<Box key="summary" flexDirection="column" rowGap={1} paddingLeft={Math.ceil(GREET_SIZE / CELL_PX) + 2}>
|
|
88
90
|
{summary.map(([name, value], i) => (
|
|
89
91
|
<Box key={'sum-' + i} flexDirection="row" columnGap={2} alignItems="flex-start">
|
|
90
|
-
<Box width={nameCells} flexShrink={0}><Text
|
|
91
|
-
<Text
|
|
92
|
+
<Box width={nameCells} flexShrink={0}><Text {...k.mute}>{name}</Text></Box>
|
|
93
|
+
<Text {...k.ink} wrap="wrap">{value}</Text>
|
|
92
94
|
</Box>
|
|
93
95
|
))}
|
|
94
96
|
</Box>,
|
|
@@ -100,7 +102,7 @@ export function renderSetup(el: Els, frame: Frame, view: SetupView, a: SetupActi
|
|
|
100
102
|
</Box>,
|
|
101
103
|
]
|
|
102
104
|
: []
|
|
103
|
-
const next = done ? pill(el, 'finish', t(L, 'setup.start'), 'primary', a.finish, 'green', 'next') : pill(el, 'next', view.step === 3 ? t(L, 'setup.finish') : t(L, 'setup.next'), 'primary', a.next, 'violet', 'next')
|
|
105
|
+
const next = done ? pill(el, frame, 'finish', t(L, 'setup.start'), 'primary', a.finish, 'green', 'next') : pill(el, frame, 'next', view.step === 3 ? t(L, 'setup.finish') : t(L, 'setup.next'), 'primary', a.next, 'violet', 'next')
|
|
104
106
|
return capsule(el, frame, {
|
|
105
107
|
key: 'setup',
|
|
106
108
|
sea: { tone: done ? 'green' : 'violet', ripples: 'calm', reach: 170, motes: true, wave: 19 * 0.5 + GREET_SIZE + 9, ...(done ? { tick: true, weather: 'sparkles' as const } : {}), at: { x: 1.5 * CELL_PX + GREET_SIZE / 2, y: 19 * 0.5 + GREET_SIZE / 2 } },
|
|
@@ -109,21 +111,21 @@ export function renderSetup(el: Els, frame: Frame, view: SetupView, a: SetupActi
|
|
|
109
111
|
gap: 1,
|
|
110
112
|
children: [
|
|
111
113
|
<Box key="setup-head" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
112
|
-
{ripple(el, 'setup', face, title, GREET_SIZE)}
|
|
114
|
+
{ripple(el, frame, 'setup', face, title, GREET_SIZE)}
|
|
113
115
|
<Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
|
|
114
116
|
<Box flexDirection="row" alignItems="center" columnGap={2} flexWrap="wrap">
|
|
115
|
-
<Text bold
|
|
117
|
+
<Text bold {...(done ? { color: k.tone('green') } : k.ink)} wrap="wrap">{title}</Text>
|
|
116
118
|
{done ? null : pic(el, 'setup-beads', beads.source, view.step + ' / 3', beads.width, 16)}
|
|
117
119
|
</Box>
|
|
118
|
-
<Text
|
|
120
|
+
<Text {...k.sub} wrap="wrap">{line}</Text>
|
|
119
121
|
</Box>
|
|
120
122
|
</Box>,
|
|
121
123
|
...content,
|
|
122
124
|
<Box key="actions" flexDirection="row" alignItems="center" columnGap={1} flexWrap="wrap" rowGap={1}>
|
|
123
125
|
{next}
|
|
124
|
-
{!done && view.step === 2 ? pill(el, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open') : null}
|
|
125
|
-
{done || view.step > 1 ? pill(el, 'back', t(L, 'setup.back'), 'ghost', a.back, 'violet', 'back') : null}
|
|
126
|
-
{done ? null : pill(el, 'skip', t(L, 'setup.skip'), 'ghost', a.skip)}
|
|
126
|
+
{!done && view.step === 2 ? pill(el, frame, 'all-settings', t(L, 'settings.openAll'), 'ghost', a.openAllSettings, 'violet', 'open') : null}
|
|
127
|
+
{done || view.step > 1 ? pill(el, frame, 'back', t(L, 'setup.back'), 'ghost', a.back, 'violet', 'back') : null}
|
|
128
|
+
{done ? null : pill(el, frame, 'skip', t(L, 'setup.skip'), 'ghost', a.skip)}
|
|
127
129
|
</Box>,
|
|
128
130
|
],
|
|
129
131
|
})
|
|
@@ -84,10 +84,13 @@ export function textTone(appearance: Appearance): typeof TEXT {
|
|
|
84
84
|
const KNOWN: Record<string, string> = {
|
|
85
85
|
anthropic: '#d97757',
|
|
86
86
|
chatgpt: '#10a37f',
|
|
87
|
+
google: '#4285f4',
|
|
87
88
|
'google-gemini': '#4285f4',
|
|
88
89
|
deepseek: '#4d6bfe',
|
|
89
90
|
'opencode-go': '#9333ea',
|
|
90
91
|
'opencode-zen': '#9333ea',
|
|
92
|
+
// xAI's mark is black on white; silver reads on the band's dark capsule.
|
|
93
|
+
grok: '#a8a29e',
|
|
91
94
|
}
|
|
92
95
|
const PALETTE = ['#0891b2', '#db2777', '#7c3aed', '#0d9488', '#c026d3', '#4f46e5', '#0284c7', '#be185d']
|
|
93
96
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// A subagent's tool calls as the Agents pane words them: one short line of each call's input, and a
|
|
2
|
+
// run of calls headed in one line the way the Claude app heads it ("명령어 7개 실행함, 파일 1개를
|
|
3
|
+
// 편집함"). Pure, so the tests can call it.
|
|
4
|
+
//
|
|
5
|
+
// The phone app's host words a run the same way (packages/remote/src/tool-details.ts
|
|
6
|
+
// `activitySummary`). The mod cannot import it: Claude Code loads the mod as its own folder
|
|
7
|
+
// (dist/mod in an npm install, scripts/build-npm.mjs), with no packages/remote beside it. So the
|
|
8
|
+
// rules are kept here too: kinds in the order they first appear, a file read or edited counted once,
|
|
9
|
+
// five or more kinds cut to the first three and the rest counted.
|
|
10
|
+
|
|
11
|
+
import { t } from '../i18n.ts'
|
|
12
|
+
import type { Lang } from '../i18n.ts'
|
|
13
|
+
|
|
14
|
+
/** Where the session runs and the user's home, to say paths short: relative to the one, `~` for the other. */
|
|
15
|
+
export type Place = { cwd?: string; home?: string }
|
|
16
|
+
|
|
17
|
+
/** One tool call as the pane holds it (view.ts AgentEntry's tool kind). */
|
|
18
|
+
export type ToolCall = { id: string; tool: string; detail: string; state: 'running' | 'ok' | 'error' }
|
|
19
|
+
|
|
20
|
+
type Activity = 'command' | 'read' | 'edit' | 'search' | 'web' | 'agent' | 'other'
|
|
21
|
+
|
|
22
|
+
export function activityOf(tool: string): Activity {
|
|
23
|
+
if (['Bash', 'BashOutput', 'KillShell'].includes(tool)) return 'command'
|
|
24
|
+
if (tool === 'Read') return 'read'
|
|
25
|
+
if (['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(tool)) return 'edit'
|
|
26
|
+
if (['Grep', 'Glob', 'LS'].includes(tool)) return 'search'
|
|
27
|
+
if (['WebSearch', 'WebFetch'].includes(tool)) return 'web'
|
|
28
|
+
if (['Task', 'Agent'].includes(tool)) return 'agent'
|
|
29
|
+
return 'other'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A directory without its trailing slash; nothing for one that cannot shorten a path (`/`, `~`). */
|
|
33
|
+
function dir(path: string | undefined): string | undefined {
|
|
34
|
+
const bare = path?.replace(/\/+$/, '')
|
|
35
|
+
return bare && bare.startsWith('/') ? bare : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A path relative to the session's folder, else under `~`, else as given. */
|
|
39
|
+
export function shortPath(path: string, place: Place): string {
|
|
40
|
+
const cwd = dir(place.cwd)
|
|
41
|
+
const home = dir(place.home)
|
|
42
|
+
if (cwd) {
|
|
43
|
+
if (path === cwd) return '.'
|
|
44
|
+
if (path.startsWith(cwd + '/')) return path.slice(cwd.length + 1)
|
|
45
|
+
}
|
|
46
|
+
if (home) {
|
|
47
|
+
if (path === home) return '~'
|
|
48
|
+
if (path.startsWith(home + '/')) return '~' + path.slice(home.length)
|
|
49
|
+
}
|
|
50
|
+
return path
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function escape(text: string): string {
|
|
54
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Every absolute path in a command said short, as shortPath says one. */
|
|
58
|
+
function shortPaths(text: string, place: Place): string {
|
|
59
|
+
let out = text
|
|
60
|
+
const cwd = dir(place.cwd)
|
|
61
|
+
const home = dir(place.home)
|
|
62
|
+
if (cwd) out = out.replace(new RegExp(escape(cwd) + '(/|(?![\\w.-]))', 'g'), (_m, slash: string) => slash ? '' : '.')
|
|
63
|
+
if (home) out = out.replace(new RegExp(escape(home) + '(?=/|(?![\\w.-]))', 'g'), '~')
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A `cd <dir> &&` (or `;`, or a line of its own) at the head of a command: it says nothing about what it does. */
|
|
68
|
+
const CD = /^\s*cd\s+(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s*(?:&&|;|\n)\s*/
|
|
69
|
+
|
|
70
|
+
/** A command as a line: without the `cd` it opens with, its first line only, its paths short. */
|
|
71
|
+
export function commandLine(command: string, place: Place = {}): string {
|
|
72
|
+
let rest = command
|
|
73
|
+
for (let i = 0; i < 3 && CD.test(rest); i++) rest = rest.replace(CD, '')
|
|
74
|
+
const first = rest.split('\n').find(line => line.trim()) ?? ''
|
|
75
|
+
return shortPaths(first.trim(), place)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function text(value: unknown): string {
|
|
79
|
+
return typeof value === 'string' ? value : value === undefined || value === null ? '' : JSON.stringify(value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function firstLine(value: string): string {
|
|
83
|
+
return (value.split('\n').find(line => line.trim()) ?? '').trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A tool call's input in one line: a command's own description when it gave one, else the command;
|
|
88
|
+
* a file's path; a search's pattern (and where). Paths short, multi-line input its first line.
|
|
89
|
+
*/
|
|
90
|
+
export function toolDetail(tool: string, input: Readonly<Record<string, unknown>>, place: Place = {}): string {
|
|
91
|
+
if (tool === 'Bash') {
|
|
92
|
+
const description = firstLine(text(input.description))
|
|
93
|
+
if (description) return description
|
|
94
|
+
return commandLine(text(input.command), place)
|
|
95
|
+
}
|
|
96
|
+
if (tool === 'Grep' || tool === 'Glob') {
|
|
97
|
+
const where = text(input.path)
|
|
98
|
+
return [firstLine(text(input.pattern)), where ? shortPath(where, place) : ''].filter(Boolean).join(' · ')
|
|
99
|
+
}
|
|
100
|
+
const path = text(input.file_path ?? input.notebook_path ?? input.path)
|
|
101
|
+
if (path) return shortPath(firstLine(path), place)
|
|
102
|
+
const detail = input.command ?? input.pattern ?? input.query ?? input.url ?? input.description ?? input.prompt ?? ''
|
|
103
|
+
return shortPaths(firstLine(text(detail)), place)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A run's head in one line, and how many of its calls failed. */
|
|
107
|
+
export function runSummary(run: readonly ToolCall[], lang: Lang): { text: string; failed: number } {
|
|
108
|
+
const counts = new Map<Activity, number>()
|
|
109
|
+
const files = new Set<string>()
|
|
110
|
+
for (const call of run) {
|
|
111
|
+
const kind = activityOf(call.tool)
|
|
112
|
+
const file = (kind === 'read' || kind === 'edit') && call.detail ? kind + ':' + call.detail : ''
|
|
113
|
+
if (!file || !files.has(file)) counts.set(kind, (counts.get(kind) ?? 0) + 1)
|
|
114
|
+
if (file) files.add(file)
|
|
115
|
+
}
|
|
116
|
+
const word = (key: string, count: number) => t(lang, 'agents.act.' + key + (count === 1 ? 'One' : ''), { count })
|
|
117
|
+
const parts = [...counts]
|
|
118
|
+
const shown = parts.length >= 5 ? parts.slice(0, 3) : parts
|
|
119
|
+
const rest = parts.slice(shown.length).reduce((sum, [, n]) => sum + n, 0)
|
|
120
|
+
return {
|
|
121
|
+
text: [...shown.map(([kind, n]) => word(kind, n)), ...(rest ? [word('more', rest)] : [])].join(', '),
|
|
122
|
+
failed: run.filter(call => call.state === 'error').length,
|
|
123
|
+
}
|
|
124
|
+
}
|