@scrylog/cli 0.3.0 → 0.3.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.
Files changed (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
@@ -1,256 +0,0 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import type { SessionListItem } from "../../shared/session-view";
3
- import { BRAND, BRAND_MARK } from "../theme";
4
- import { STATE_COLORS, stateGlyph } from "../model";
5
- import type { DaemonVersionMismatch } from "../../shared/version";
6
-
7
- interface DashboardHeaderProps {
8
- sessions: SessionListItem[];
9
- connected: boolean;
10
- spinnerFrame: number;
11
- width: number;
12
- versionMismatch?: DaemonVersionMismatch;
13
- }
14
-
15
- export function dashboardHeaderHeight(_width?: number): number {
16
- return 6;
17
- }
18
-
19
- function countStates(sessions: SessionListItem[]): Record<string, number> {
20
- const counts: Record<string, number> = { working: 0, idle: 0, "waiting-input": 0, error: 0, shell: 0, unknown: 0 };
21
- for (const session of sessions) {
22
- if (session.state in counts) counts[session.state]! += 1;
23
- else counts.unknown! += 1;
24
- }
25
- return counts;
26
- }
27
-
28
- const TILE_WIDTH = 13;
29
- /** Right column usable width required to render a full 3-tile row (3*13 + 2 gaps) with room. */
30
- const TILE_MIN_USABLE_FOR_IDLE = 42;
31
- /** two tiles + gap: the narrow fallback that needs no separator or idle tile. */
32
- const TILE_MIN_USABLE_FOR_INPUT = 27;
33
- /** Brand column width in narrow layouts. */
34
- const HEADER_NARROW_LEFT_WIDTH = 16;
35
- /** Narrow terminals keep the brand mark whenever working/input tiles still fit next to it. */
36
- const HEADER_NARROW_LOGO_MIN = 46;
37
-
38
- interface StatusTileProps {
39
- state: string;
40
- label: string;
41
- count: number;
42
- spinnerFrame: number;
43
- tileWidth?: number;
44
- }
45
-
46
- interface StatusBlockProps {
47
- sessions: SessionListItem[];
48
- spinnerFrame: number;
49
- showSeparator: boolean;
50
- narrow: boolean;
51
- usable: number;
52
- /** Idle tile may only render when the brand mark is reserved beside 3 tiles. */
53
- allowIdle: boolean;
54
- /** Brand mark is hidden: working/input stretch to fill the full row. */
55
- stretch: boolean;
56
- }
57
-
58
- function StatusBlock({ sessions, spinnerFrame, showSeparator, narrow, usable, allowIdle, stretch }: StatusBlockProps) {
59
- const counts = countStates(sessions);
60
- let tiles: StatusTileProps[] = [
61
- { state: "working", label: "working", count: counts.working ?? 0, spinnerFrame },
62
- { state: "waiting-input", label: "input", count: (counts["waiting-input"] ?? 0) + (counts.error ?? 0), spinnerFrame },
63
- { state: "idle", label: "idle", count: counts.idle ?? 0, spinnerFrame },
64
- ];
65
- if (narrow) {
66
- // working/input are always visible (tinted when inactive); idle only shows
67
- // when the full 3-tile row fits next to the brand mark.
68
- tiles = tiles.filter((tile) => {
69
- if (tile.state === "working") return true;
70
- if (tile.state === "waiting-input") return usable >= TILE_MIN_USABLE_FOR_INPUT;
71
- return allowIdle && tile.count > 0;
72
- });
73
- }
74
- if (tiles.length === 0) {
75
- return null;
76
- }
77
- const blockWidth = stretch
78
- ? usable
79
- : tiles.length * TILE_WIDTH + (tiles.length - 1);
80
- const tileWidth = stretch ? Math.floor((usable - 1) / 2) : TILE_WIDTH;
81
- return (
82
- <box flexDirection="column" height={showSeparator ? 5 : 4}>
83
- <box flexDirection="row" height={4} gap={1}>
84
- {tiles.map((tile) => (
85
- <StatusTile key={tile.state} {...tile} tileWidth={tileWidth} />
86
- ))}
87
- </box>
88
- {showSeparator && <TileSeparator width={blockWidth} />}
89
- </box>
90
- );
91
- }
92
-
93
- function StatusTile({ state, label, count, spinnerFrame, tileWidth = TILE_WIDTH }: StatusTileProps) {
94
- const active = count > 0;
95
- const bg = active ? STATE_COLORS[state] : undefined;
96
- const fg = active ? BRAND.bgDark : STATE_COLORS[state];
97
- const spinner = active ? stateGlyph(state, spinnerFrame) : stateGlyph(state);
98
- return (
99
- <box
100
- width={tileWidth}
101
- height={4}
102
- backgroundColor={bg}
103
- flexDirection="column"
104
- justifyContent="center"
105
- alignItems="center"
106
- >
107
- <text attributes={TextAttributes.BOLD} fg={fg} content={`${count}`} maxWidth={tileWidth} />
108
- <text fg={fg} content={`${spinner} ${label}`} maxWidth={tileWidth} />
109
- </box>
110
- );
111
- }
112
-
113
- function TileSeparator({ width }: { width: number }) {
114
- return (
115
- <box height={1}>
116
- <text fg={BRAND.border} content={"─".repeat(width)} />
117
- </box>
118
- );
119
- }
120
-
121
- function StatusItem({ state, count, spinnerFrame, width }: { state: string; count: number; spinnerFrame: number; width?: number }) {
122
- const label = state === "waiting-input" ? "input" : state;
123
- return (
124
- <text
125
- width={width}
126
- fg={STATE_COLORS[state] ?? BRAND.grey}
127
- content={`${stateGlyph(state, spinnerFrame)} (${label}): ${count}`}
128
- />
129
- );
130
- }
131
-
132
- function StatusFooterRow({ sessions, spinnerFrame, narrow, usable }: { sessions: SessionListItem[]; spinnerFrame: number; narrow: boolean; usable: number }) {
133
- const counts = countStates(sessions);
134
- let avail = usable - 1;
135
- const fits = (size: number) => {
136
- if (!narrow) return true;
137
- if (avail < size) return false;
138
- avail -= size;
139
- return true;
140
- };
141
- const showShell = fits(15);
142
- const showUnknown = fits(14);
143
- const showTotal = fits(10);
144
- return (
145
- <box flexDirection="row" height={1} paddingLeft={1}>
146
- {showShell && <StatusItem state="shell" count={counts.shell ?? 0} spinnerFrame={spinnerFrame} width={15} />}
147
- {showUnknown && <StatusItem state="unknown" count={counts.unknown ?? 0} spinnerFrame={spinnerFrame} width={14} />}
148
- {showTotal && <text width={12} content={` total: ${sessions.length}`} />}
149
- </box>
150
- );
151
- }
152
-
153
- function BrandMark() {
154
- return (
155
- <box flexDirection="column" height={5}>
156
- {BRAND_MARK.art.map((line, index) => (
157
- <text key={index} fg={line.fg} content={line.content} />
158
- ))}
159
- </box>
160
- );
161
- }
162
-
163
- const SCRY_BLOCKS = " █▀ █▀▀ █▀█ █▄█\n ▄█ █▄▄ █▀▄ ░█░";
164
- const LOG_BLOCKS = " █░ █▀█ █▀▀\n █▄ █▄█ █▄█";
165
-
166
- function Wordmark({ width }: { width: number }) {
167
- if (width < 72) {
168
- return (
169
- <box flexDirection="row">
170
- <text attributes={TextAttributes.BOLD} fg={BRAND.primary} content=" SCRY" />
171
- <text attributes={TextAttributes.BOLD} content="LOG" />
172
- </box>
173
- );
174
- }
175
- return (
176
- <box flexDirection="row">
177
- <text height={2} fg={BRAND.primary} content={SCRY_BLOCKS} />
178
- <text height={2} fg={BRAND.grey} content={LOG_BLOCKS} />
179
- </box>
180
- );
181
- }
182
-
183
- function DaemonWarning({ connected, mismatch, narrow, width }: { connected: boolean; mismatch?: DaemonVersionMismatch; narrow: boolean; width: number }) {
184
- if (!connected) {
185
- return (
186
- <box height={1}>
187
- <text fg={BRAND.danger} content=" ● daemon disconnected" />
188
- </box>
189
- );
190
- }
191
- if (!mismatch) return null;
192
- const content = width < 26
193
- ? " restart daemon"
194
- : narrow
195
- ? " scrylog daemon restart"
196
- : mismatch.daemonVersion
197
- ? ` daemon ${mismatch.daemonVersion} != CLI ${mismatch.cliVersion} · scrylog daemon restart`
198
- : " old daemon · scrylog daemon restart";
199
- return (
200
- <box height={1}>
201
- <text fg={BRAND.warning} content={content} />
202
- </box>
203
- );
204
- }
205
-
206
- export function DashboardHeader(props: DashboardHeaderProps) {
207
- const narrow = props.width < 110;
208
- const leftWidth = Math.floor(Math.max(20, props.width - 2) / 2);
209
- const showLogo = !narrow || props.width >= HEADER_NARROW_LOGO_MIN;
210
- const usable = narrow
211
- ? Math.max(0, props.width - (showLogo ? HEADER_NARROW_LEFT_WIDTH + 1 : 0) - 2)
212
- : props.width;
213
- const allowIdle = showLogo && usable >= TILE_MIN_USABLE_FOR_IDLE;
214
- const showWarning = !props.connected || Boolean(props.versionMismatch);
215
- return (
216
- <box height={dashboardHeaderHeight(props.width)} flexDirection="column">
217
- <box flexDirection="row" height={6}>
218
- {showLogo && (
219
- <box
220
- width={narrow ? HEADER_NARROW_LEFT_WIDTH : leftWidth}
221
- flexShrink={0}
222
- flexDirection="row"
223
- gap={1}
224
- paddingLeft={1}
225
- >
226
- <BrandMark />
227
- {!narrow && (
228
- <box flexDirection="column" justifyContent="center">
229
- <Wordmark width={props.width} />
230
- </box>
231
- )}
232
- </box>
233
- )}
234
- <box
235
- {...(narrow ? { width: Math.max(0, props.width - (showLogo ? HEADER_NARROW_LEFT_WIDTH : 0)) } : { flexGrow: 1 })}
236
- flexDirection="column"
237
- justifyContent="flex-start"
238
- alignItems="flex-end"
239
- paddingRight={2}
240
- >
241
- <DaemonWarning connected={props.connected} mismatch={props.versionMismatch} narrow={narrow} width={props.width} />
242
- <StatusBlock
243
- sessions={props.sessions}
244
- spinnerFrame={props.spinnerFrame}
245
- showSeparator={!showWarning}
246
- narrow={narrow}
247
- usable={usable}
248
- allowIdle={allowIdle}
249
- stretch={narrow && !showLogo}
250
- />
251
- <StatusFooterRow sessions={props.sessions} spinnerFrame={props.spinnerFrame} narrow={narrow} usable={usable} />
252
- </box>
253
- </box>
254
- </box>
255
- );
256
- }
@@ -1,67 +0,0 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import { truncate } from "../../shared/utils";
3
- import type { DashboardView } from "../model";
4
- import { contextBindings, essentialBindings } from "../keybindings";
5
- import { BRAND } from "../theme";
6
-
7
- const KEY_COLOR = BRAND.primary;
8
- const DESCRIPTION_COLOR = BRAND.grey;
9
- const SEPARATOR = " · ";
10
-
11
- function KeyBarItem({ binding, separator }: { binding: { key: string; description: string }; separator?: string }) {
12
- return (
13
- <box flexDirection="row" height={1} flexShrink={0} gap={1}>
14
- <text fg={KEY_COLOR} attributes={TextAttributes.BOLD} content={binding.key} />
15
- <text fg={DESCRIPTION_COLOR} content={binding.description} />
16
- {separator && <text fg={DESCRIPTION_COLOR} content={separator} />}
17
- </box>
18
- );
19
- }
20
-
21
- export function KeyBar({
22
- view,
23
- width,
24
- context,
25
- }: {
26
- view: DashboardView;
27
- width: number;
28
- context?: string;
29
- }) {
30
- const bindings = contextBindings(view).filter((binding) => binding.footer !== false);
31
- const essentials = essentialBindings(view).filter((binding) => binding.footer !== false);
32
- const narrow = width < 110;
33
- const candidates = narrow ? essentials : bindings;
34
- const contextSuffix = context ? `${SEPARATOR}${context}` : "";
35
- const maxHidden = Math.max(0, bindings.length - essentials.length);
36
- const indicatorWidth = narrow && maxHidden > 0 ? SEPARATOR.length + `+${maxHidden} more (?)`.length : 0;
37
- const budget = Math.max(0, width - 2 - contextSuffix.length - indicatorWidth);
38
-
39
- const items: { key: string; description: string }[] = [];
40
- let used = 0;
41
- for (const binding of candidates) {
42
- const itemWidth = binding.key.length + 1 + binding.description.length + (items.length > 0 ? SEPARATOR.length : 0);
43
- if (items.length > 0 && used + itemWidth > budget) break;
44
- items.push(binding);
45
- used += itemWidth;
46
- }
47
-
48
- const hidden = bindings.length - items.length;
49
- const indicator = hidden > 0 ? `+${hidden} more (?)` : null;
50
-
51
- return (
52
- <box height={1} flexDirection="row" width={Math.max(1, width - 1)} paddingX={1} flexShrink={0}>
53
- {items.map((binding, index) => (
54
- <KeyBarItem key={binding.key} binding={binding} separator={index < items.length - 1 ? SEPARATOR : undefined} />
55
- ))}
56
- {indicator && <text fg={DESCRIPTION_COLOR} content={`${SEPARATOR}${indicator}`} />}
57
- {context && (
58
- <text
59
- flexGrow={1}
60
- fg={DESCRIPTION_COLOR}
61
- content={truncate(contextSuffix, Math.max(1, width - 2 - used))}
62
- truncate
63
- />
64
- )}
65
- </box>
66
- );
67
- }
@@ -1,169 +0,0 @@
1
- import { TextAttributes } from "@opentui/core";
2
- import type { SessionModel } from "../../shared/types";
3
- import type { TuiConfig } from "../../shared/config";
4
- import { formatDuration, truncate } from "../../shared/utils";
5
- import { BRAND } from "../theme";
6
- import { STATE_COLORS } from "../model";
7
- import { providerAccentColor, providerVisual, ProviderIcon } from "./UsageView";
8
- import { MINI_ICON_SIZE, isImageIconMode, toolProviderKey } from "./tool-icons";
9
-
10
- const LABEL_WIDTH = 12;
11
-
12
- interface DetailRow {
13
- label: string;
14
- value: string;
15
- color?: string;
16
- }
17
-
18
- function formatRelativeTime(timestamp: number, now = Date.now()): string {
19
- const seconds = Math.max(1, Math.round((now - timestamp) / 1000));
20
- if (seconds > 86400) return `${Math.floor(seconds / 86400)}d ago`;
21
- if (seconds > 3600) return `${Math.floor(seconds / 3600)}h ago`;
22
- if (seconds > 60) return `${Math.floor(seconds / 60)}m ago`;
23
- return "just now";
24
- }
25
-
26
- function buildInfoRows(session: SessionModel): DetailRow[] {
27
- const rows: DetailRow[] = [];
28
- rows.push({ label: "id", value: session.id });
29
- rows.push({ label: "state", value: `${session.state} (${formatDuration(session.stableForMs ?? 0)})`, color: STATE_COLORS[session.state] ?? BRAND.fallback });
30
- rows.push({ label: "tool", value: session.tool });
31
- rows.push({ label: "source", value: `${session.source}${session.detectionSource ? ` / ${session.detectionSource}` : ""}` });
32
- if (session.project?.displayName) rows.push({ label: "project", value: session.project.displayName });
33
- if (session.project?.isGitRepository && session.project?.branch) {
34
- rows.push({ label: "git", value: session.project.branch, color: BRAND.accentBlue });
35
- }
36
- if (session.currentPath) rows.push({ label: "path", value: session.currentPath });
37
- if (session.summary) rows.push({ label: "summary", value: session.summary });
38
- rows.push({ label: "activity", value: formatRelativeTime(session.lastActivity) });
39
- if (session.processChain?.length) {
40
- rows.push({ label: "command", value: session.processChain.join(" → "), color: BRAND.grey });
41
- }
42
- if (session.tmux) {
43
- rows.push({ label: "tmux", value: `${session.tmux.session}:${session.tmux.window}.${session.tmux.pane}`, color: BRAND.accentBlue });
44
- if (session.tmux.windowName) rows.push({ label: "window", value: session.tmux.windowName });
45
- }
46
- if (session.herdr) {
47
- const herdr = session.herdr;
48
- if (herdr.workspace) rows.push({ label: "workspace", value: herdr.workspace, color: BRAND.accentBlue });
49
- if (herdr.tab) rows.push({ label: "tab", value: herdr.tab });
50
- if (herdr.name) rows.push({ label: "pane", value: herdr.name });
51
- if (herdr.agent) {
52
- rows.push({ label: "agent", value: herdr.agent + (herdr.agentStatus ? ` (${herdr.agentStatus})` : ""), color: BRAND.accentBlue });
53
- }
54
- }
55
- if (session.opencode) {
56
- rows.push({ label: "instance", value: session.opencode.instanceUrl, color: BRAND.grey });
57
- }
58
- if (session.claude?.permissionMode) rows.push({ label: "perm mode", value: session.claude.permissionMode });
59
- if (session.codex?.model) rows.push({ label: "model", value: session.codex.model });
60
- if (session.codex?.permissionMode) rows.push({ label: "perm mode", value: session.codex.permissionMode });
61
- if (session.parentSessionId) rows.push({ label: "parent", value: session.parentSessionId });
62
- if (session.childSessionIds?.length) rows.push({ label: "children", value: String(session.childSessionIds.length) });
63
- return rows;
64
- }
65
-
66
- export function SessionDetailModal(props: {
67
- detail: SessionModel;
68
- iconMode: TuiConfig["providerIcons"];
69
- width: number;
70
- }) {
71
- const { detail } = props;
72
- const toolMode = isImageIconMode(props.iconMode) ? "icons" : props.iconMode === "text" ? "text" : "monogram";
73
- const toolKey = toolProviderKey(detail.tool);
74
- const stateColor = STATE_COLORS[detail.state] ?? BRAND.fallback;
75
- const modalWidth = Math.max(40, Math.floor(props.width * 0.9));
76
- const innerWidth = Math.max(20, modalWidth - 4);
77
- const titleWidth = Math.max(10, innerWidth - 12);
78
-
79
- const infoRows = buildInfoRows(detail);
80
- const reasonRows = [
81
- ...(detail.toolReasons ?? []).map((reason) => ({ kind: "tool" as const, value: `${reason.label} — ${reason.detail}` })),
82
- ...(detail.stateReasons ?? []).map((reason) => ({ kind: "state" as const, value: `${reason.label} — ${reason.detail}` })),
83
- ];
84
- const longestValue = Math.max(
85
- 0,
86
- ...infoRows.map((row) => row.value.length),
87
- ...reasonRows.map((reason) => reason.value.length),
88
- );
89
- const tableWidth = Math.max(innerWidth, LABEL_WIDTH + 2 + longestValue);
90
- const tableRule = `${"─".repeat(LABEL_WIDTH)}┼${"─".repeat(tableWidth - LABEL_WIDTH - 1)}`;
91
-
92
- return (
93
- <box
94
- position="absolute"
95
- top="5%"
96
- left="5%"
97
- width="90%"
98
- height="90%"
99
- title="Session detail"
100
- border
101
- borderColor={BRAND.primary}
102
- backgroundColor={BRAND.surface}
103
- flexDirection="column"
104
- padding={1}
105
- zIndex={10}
106
- >
107
- <box flexGrow={1} overflow="hidden" flexDirection="column">
108
- <scrollbox flexGrow={1} height="100%" scrollY scrollX focused>
109
- <box flexDirection="column">
110
- <box flexDirection="row" height={1}>
111
- {toolMode === "icons" && toolKey ? (
112
- <>
113
- <ProviderIcon provider={toolKey} mode={props.iconMode} size={MINI_ICON_SIZE} />
114
- <text width={1} content=" " />
115
- <text
116
- fg={providerAccentColor(toolKey)}
117
- attributes={TextAttributes.BOLD}
118
- content={providerVisual(toolKey).short}
119
- />
120
- </>
121
- ) : (
122
- <text
123
- fg={toolKey ? providerAccentColor(toolKey) : BRAND.fallback}
124
- attributes={TextAttributes.BOLD}
125
- content={toolKey ? providerVisual(toolKey).short : truncate(detail.tool, 8)}
126
- />
127
- )}
128
- <text width={1} content=" " />
129
- <text
130
- fg={BRAND.fallback}
131
- attributes={TextAttributes.BOLD}
132
- content={truncate(detail.project?.displayName ?? detail.id, titleWidth)}
133
- truncate
134
- />
135
- <text fg={BRAND.grey} content=" · " />
136
- <text fg={stateColor} attributes={TextAttributes.BOLD} content={detail.state} />
137
- </box>
138
- <box height={1} flexDirection="row"><text fg={BRAND.border} content={tableRule} /></box>
139
- {infoRows.map((row, index) => (
140
- <box key={row.label} flexDirection="column">
141
- <box flexDirection="row" height={1}>
142
- <text width={LABEL_WIDTH} fg={BRAND.grey} content={row.label} />
143
- <text width={1} fg={BRAND.border} content="│" />
144
- <text width={1} content=" " />
145
- <text width={row.value.length} fg={row.color ?? BRAND.fallback} content={row.value} />
146
- </box>
147
- <box height={1} flexDirection="row"><text fg={BRAND.border} content={tableRule} /></box>
148
- {index === infoRows.length - 1 && reasonRows.length > 0 && <text content="" height={1} />}
149
- </box>
150
- ))}
151
- {reasonRows.length > 0 && <text fg={BRAND.grey} attributes={TextAttributes.BOLD} content="Classification" />}
152
- {reasonRows.map((reason, index) => (
153
- <box key={`${reason.kind}:${reason.value}:${index}`} flexDirection="column">
154
- <box flexDirection="row" height={1}>
155
- <text width={LABEL_WIDTH} fg={BRAND.grey} content={reason.kind} />
156
- <text width={1} fg={BRAND.border} content="│" />
157
- <text width={1} content=" " />
158
- <text width={reason.value.length} fg={BRAND.fallback} content={reason.value} />
159
- </box>
160
- <box height={1} flexDirection="row"><text fg={BRAND.border} content={tableRule} /></box>
161
- </box>
162
- ))}
163
- </box>
164
- </scrollbox>
165
- </box>
166
- <text height={1} flexShrink={0} fg={BRAND.grey} content="←→ scroll · q/Esc close · t attach" />
167
- </box>
168
- );
169
- }
@@ -1,184 +0,0 @@
1
- import { TextAttributes, type MouseEvent } from "@opentui/core";
2
- import { useRef } from "react";
3
- import type { TuiConfig } from "../../shared/config";
4
- import type { SessionModel } from "../../shared/types";
5
- import type { SessionListItem } from "../../shared/session-view";
6
- import type { UsageState } from "../../usage/types";
7
- import { UsageSummaryPane } from "./UsageSummaryPane";
8
- import { providerAccentColor, providerVisual, ProviderIcon } from "./UsageView";
9
- import { SessionDetailModal } from "./SessionDetailModal";
10
- import { MINI_ICON_SIZE, isImageIconMode, toolProviderKey } from "./tool-icons";
11
- import { truncate } from "../../shared/utils";
12
- import { BRAND } from "../theme";
13
- import {
14
- STATE_COLORS,
15
- activeChildCounts,
16
- formatStatusCell,
17
- formatTargetLabel,
18
- sessionColumnWidths,
19
- type DashboardFilters,
20
- type DashboardMode,
21
- type DashboardRow,
22
- type SessionToolMode,
23
- } from "../model";
24
-
25
- interface SessionsViewProps {
26
- rows: DashboardRow[];
27
- sessions: SessionListItem[];
28
- selectedSessionId?: string;
29
- filters: DashboardFilters;
30
- mode: DashboardMode;
31
- detail?: SessionModel;
32
- message?: string;
33
- usage?: UsageState;
34
- iconMode: TuiConfig["providerIcons"];
35
- spinnerFrame: number;
36
- width: number;
37
- interactive?: boolean;
38
- onQueryChange(query: string): void;
39
- onSearchSubmit(): void;
40
- onSelectSession?(sessionId: string): void;
41
- onOpenSessionDetail?(session: SessionListItem): void;
42
- onScrollSelection?(direction: 1 | -1): void;
43
- }
44
-
45
- export function SessionsView(props: SessionsViewProps) {
46
- const { width } = props;
47
- const narrow = width < 110;
48
- const toolMode: SessionToolMode = isImageIconMode(props.iconMode) ? "icons" : props.iconMode === "text" ? "text" : "monogram";
49
- const childCounts = activeChildCounts(props.sessions);
50
- // Smallest tool column that fits the widest rendered content: only relevant
51
- // when the summary column is gone (width < 100).
52
- let toolContentWidth = 0;
53
- if (narrow && width < 100) {
54
- for (const row of props.rows) {
55
- if (row.kind !== "session") continue;
56
- const badgeValue = childCounts.get(row.session.id) ?? 0;
57
- const badgeText = badgeValue > 0 ? `[+${badgeValue}]` : "";
58
- let len: number;
59
- if (toolMode === "monogram") {
60
- const key = toolProviderKey(row.session.tool);
61
- const short = key ? providerVisual(key).short.length : row.session.tool.length;
62
- len = short + (badgeText ? badgeText.length + 1 : 0);
63
- } else if (toolMode === "icons") {
64
- len = MINI_ICON_SIZE.width + (badgeText ? badgeText.length + 1 : 0);
65
- } else {
66
- len = row.session.tool.length + (badgeText ? badgeText.length + 1 : 0);
67
- }
68
- toolContentWidth = Math.max(toolContentWidth, len);
69
- }
70
- }
71
- const columns = sessionColumnWidths(width, toolMode, toolContentWidth);
72
- const contentWidth = Math.max(20, width - 4);
73
- const lastClickRef = useRef<{ id: string; time: number } | undefined>(undefined);
74
-
75
- const handleRowMouseUp = props.interactive && props.onSelectSession
76
- ? (session: SessionListItem) => (event: MouseEvent) => {
77
- if (event.button !== 0) return;
78
- const now = Date.now();
79
- const last = lastClickRef.current;
80
- if (last && last.id === session.id && now - last.time < 500) {
81
- lastClickRef.current = undefined;
82
- props.onOpenSessionDetail?.(session);
83
- return;
84
- }
85
- lastClickRef.current = { id: session.id, time: now };
86
- props.onSelectSession!(session.id);
87
- }
88
- : undefined;
89
-
90
- const handleMouseScroll = props.interactive && props.onScrollSelection
91
- ? (event: MouseEvent) => {
92
- props.onScrollSelection!(event.scroll?.direction === "down" ? 1 : -1);
93
- }
94
- : undefined;
95
-
96
- return (
97
- <box position="relative" flexDirection="column" flexGrow={1}>
98
- {props.mode === "search" && (
99
- <box height={3} border borderColor={BRAND.primary} title="Search" paddingX={1}>
100
- <input
101
- focused
102
- value={props.filters.query}
103
- placeholder="session, project, tool..."
104
- onInput={props.onQueryChange}
105
- onSubmit={props.onSearchSubmit}
106
- />
107
- </box>
108
- )}
109
- <box
110
- border
111
- borderColor={BRAND.border}
112
- flexDirection="column"
113
- flexGrow={1}
114
- paddingX={1}
115
- onMouseScroll={handleMouseScroll}
116
- >
117
- <box flexDirection="row" height={1}>
118
- <text width={2} content="" />
119
- <text width={columns.session} attributes={TextAttributes.BOLD} content="Session" />
120
- <text width={columns.state} attributes={TextAttributes.BOLD} content="St" />
121
- <text width={columns.tool} attributes={TextAttributes.BOLD} content={isImageIconMode(props.iconMode) ? "" : "Tool"} truncate />
122
- {columns.summary > 0 && <text width={columns.summary} attributes={TextAttributes.BOLD} content="Summary" />}
123
- </box>
124
- {props.rows.length === 0 && <text fg={BRAND.grey} content="No sessions detected" />}
125
- {props.rows.map((row) => {
126
- if (row.kind === "gap") return <text key={row.projectKey} content="" />;
127
- if (row.kind === "project-header") {
128
- const label = truncate(row.projectName, Math.max(8, columns.session - 6));
129
- return <text key={`header:${row.projectKey}`} fg={BRAND.grey} attributes={TextAttributes.BOLD} content={` == ${label.padEnd(Math.max(8, columns.session - 6))} ==`} />;
130
- }
131
- const selected = row.session.id === props.selectedSessionId;
132
- const status = formatStatusCell(row.session, props.spinnerFrame);
133
- const childCount = narrow ? childCounts.get(row.session.id) ?? 0 : 0;
134
- const badge = childCount > 0 ? `[+${childCount}]` : "";
135
- const toolKey = toolProviderKey(row.session.tool);
136
- const toolContent = badge
137
- ? `${row.session.tool.slice(0, Math.max(1, columns.tool - badge.length - 1))} ${badge}`
138
- : row.session.tool;
139
- return (
140
- <box
141
- key={row.session.id}
142
- backgroundColor={selected ? BRAND.selection : undefined}
143
- flexDirection="row"
144
- height={1}
145
- onMouseUp={handleRowMouseUp?.(row.session)}
146
- >
147
- <text width={2} attributes={selected ? TextAttributes.BOLD : TextAttributes.NONE} content={selected ? "> " : " "} />
148
- <text width={columns.session} fg={STATE_COLORS[row.session.state] ?? BRAND.fallback} content={truncate(formatTargetLabel(row.session, narrow), columns.session)} truncate />
149
- <text width={columns.state} fg={STATE_COLORS[row.session.state] ?? BRAND.fallback} attributes={TextAttributes.BOLD} content={truncate(status, columns.state)} truncate />
150
- {toolMode === "icons" && toolKey ? (
151
- <>
152
- <ProviderIcon provider={toolKey} mode={props.iconMode} size={MINI_ICON_SIZE} />
153
- <text width={columns.tool - MINI_ICON_SIZE.width} fg={STATE_COLORS[row.session.state] ?? BRAND.fallback} content={truncate(badge, columns.tool - MINI_ICON_SIZE.width)} truncate />
154
- </>
155
- ) : toolMode === "monogram" && toolKey ? (
156
- <text
157
- width={columns.tool}
158
- fg={providerAccentColor(toolKey)}
159
- attributes={TextAttributes.BOLD}
160
- content={truncate(badge ? `${providerVisual(toolKey).short} ${badge}` : providerVisual(toolKey).short, columns.tool)}
161
- truncate
162
- />
163
- ) : (
164
- <text width={columns.tool} fg={STATE_COLORS[row.session.state] ?? BRAND.fallback} content={truncate(toolContent, columns.tool)} truncate />
165
- )}
166
- {columns.summary > 0 && (
167
- <text width={columns.summary} fg={STATE_COLORS[row.session.state] ?? BRAND.fallback} content={truncate(row.session.summary || "-", columns.summary)} truncate />
168
- )}
169
- </box>
170
- );
171
- })}
172
- </box>
173
- {props.usage && <UsageSummaryPane usage={props.usage} width={width} iconMode={props.iconMode} />}
174
- {props.mode === "detail" && props.detail && (
175
- <SessionDetailModal detail={props.detail} iconMode={props.iconMode} width={width} />
176
- )}
177
- {props.mode === "message" && props.message && (
178
- <box position="absolute" top="35%" left="15%" width="70%" height={7} title="scrylog" border borderColor={BRAND.warning} backgroundColor={BRAND.surface} padding={1} alignItems="center" justifyContent="center" zIndex={10}>
179
- <text fg={BRAND.warning} content={props.message} wrapMode="word" />
180
- </box>
181
- )}
182
- </box>
183
- );
184
- }