@scrylog/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +46 -7
  2. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  3. package/dist/assets/highlights-ghv9g403.scm +205 -0
  4. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  5. package/dist/assets/highlights-r812a2qc.scm +150 -0
  6. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  7. package/dist/assets/injections-73j83es3.scm +27 -0
  8. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  9. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  10. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/dist/cli/assets/providers/SOURCES.md +21 -0
  16. package/dist/cli/assets/providers/claude-terminal.png +0 -0
  17. package/dist/cli/assets/providers/claude-terminal.svg +62 -0
  18. package/dist/cli/assets/providers/claude.png +0 -0
  19. package/dist/cli/assets/providers/copilot.png +0 -0
  20. package/dist/cli/assets/providers/copilot.svg +44 -0
  21. package/dist/cli/assets/providers/openai-terminal.png +0 -0
  22. package/dist/cli/assets/providers/openai-terminal.svg +62 -0
  23. package/dist/cli/assets/providers/openai.png +0 -0
  24. package/dist/cli/assets/providers/opencode-terminal.png +0 -0
  25. package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
  26. package/dist/cli/assets/providers/opencode.png +0 -0
  27. package/dist/cli/assets/providers/shell.png +0 -0
  28. package/dist/cli/assets/providers/shell.svg +23 -0
  29. package/dist/cli/assets/providers/unknown.png +0 -0
  30. package/dist/cli/assets/providers/unknown.svg +23 -0
  31. package/dist/cli/index.js +499 -0
  32. package/dist/cli/index.js.map +61 -0
  33. package/dist/daemon/index.js +670 -0
  34. package/dist/daemon/index.js.map +1475 -0
  35. package/dist/hooks/claude.js +6 -0
  36. package/dist/hooks/claude.js.map +11 -0
  37. package/dist/hooks/codex.js +10 -0
  38. package/dist/hooks/codex.js.map +11 -0
  39. package/dist/mcp/index.js +100 -0
  40. package/dist/mcp/index.js.map +231 -0
  41. package/dist/oc/index.js +21 -0
  42. package/dist/oc/index.js.map +10 -0
  43. package/package.json +26 -20
  44. package/src/postinstall.cjs +11 -0
  45. package/src/api/docs/sessions.keys.ts +0 -6
  46. package/src/api/docs/sessions.ts +0 -81
  47. package/src/api/docs/system.keys.ts +0 -5
  48. package/src/api/docs/system.ts +0 -66
  49. package/src/api/docs/usage.keys.ts +0 -3
  50. package/src/api/docs/usage.ts +0 -24
  51. package/src/api/docs/webhooks.keys.ts +0 -5
  52. package/src/api/docs/webhooks.ts +0 -78
  53. package/src/api/index.ts +0 -18
  54. package/src/api/lib/auth.ts +0 -43
  55. package/src/api/lib/errors.ts +0 -28
  56. package/src/api/lib/route.ts +0 -18
  57. package/src/api/models/claude.ts +0 -4
  58. package/src/api/models/codex.ts +0 -4
  59. package/src/api/models/opencode.ts +0 -11
  60. package/src/api/routes/sessions.ts +0 -47
  61. package/src/api/routes/system.ts +0 -103
  62. package/src/api/routes/usage.ts +0 -15
  63. package/src/api/routes/webhooks/claude.ts +0 -141
  64. package/src/api/routes/webhooks/codex.ts +0 -175
  65. package/src/api/routes/webhooks/opencode.ts +0 -111
  66. package/src/api/routes.ts +0 -93
  67. package/src/api/schemas/common.ts +0 -21
  68. package/src/api/schemas/sessions.ts +0 -72
  69. package/src/api/schemas/usage.ts +0 -50
  70. package/src/api/schemas/webhooks/claude.ts +0 -32
  71. package/src/api/schemas/webhooks/codex.ts +0 -34
  72. package/src/api/schemas/webhooks/opencode.ts +0 -29
  73. package/src/api/services/claude-session.ts +0 -9
  74. package/src/api/services/codex-session.ts +0 -8
  75. package/src/api/services/opencode-session.ts +0 -73
  76. package/src/classifier.ts +0 -206
  77. package/src/cli/commands.ts +0 -632
  78. package/src/cli/prompt.ts +0 -44
  79. package/src/daemon/cli.ts +0 -27
  80. package/src/daemon/collectors/claude/README.md +0 -108
  81. package/src/daemon/collectors/claude/collector.ts +0 -423
  82. package/src/daemon/collectors/claude/discovery.ts +0 -148
  83. package/src/daemon/collectors/claude/hooks.ts +0 -181
  84. package/src/daemon/collectors/claude/index.ts +0 -8
  85. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  86. package/src/daemon/collectors/claude/transcript.ts +0 -246
  87. package/src/daemon/collectors/codex/README.md +0 -147
  88. package/src/daemon/collectors/codex/collector.ts +0 -661
  89. package/src/daemon/collectors/codex/discovery.ts +0 -104
  90. package/src/daemon/collectors/codex/hooks.ts +0 -179
  91. package/src/daemon/collectors/codex/index.ts +0 -7
  92. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  93. package/src/daemon/collectors/codex/transcript.ts +0 -299
  94. package/src/daemon/collectors/git.ts +0 -295
  95. package/src/daemon/collectors/opencode/README.md +0 -106
  96. package/src/daemon/collectors/opencode/collector.ts +0 -545
  97. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  98. package/src/daemon/collectors/opencode/index.ts +0 -5
  99. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  100. package/src/daemon/collectors/opencode-db.ts +0 -282
  101. package/src/daemon/collectors/process.ts +0 -201
  102. package/src/daemon/collectors/tmux/README.md +0 -128
  103. package/src/daemon/collectors/tmux/collector.ts +0 -472
  104. package/src/daemon/collectors/tmux/enrichment.ts +0 -241
  105. package/src/daemon/collectors/tmux/index.ts +0 -4
  106. package/src/daemon/collectors/tmux/parse.ts +0 -128
  107. package/src/daemon/collectors/usage.ts +0 -193
  108. package/src/daemon/index.ts +0 -207
  109. package/src/daemon/scheduler.ts +0 -86
  110. package/src/daemon/store.ts +0 -588
  111. package/src/detect.ts +0 -143
  112. package/src/hooks/claude.ts +0 -596
  113. package/src/hooks/codex.ts +0 -517
  114. package/src/legacy/classifier.ts +0 -215
  115. package/src/legacy/detect.ts +0 -143
  116. package/src/mcp/client-server.ts +0 -21
  117. package/src/mcp/http-handler.ts +0 -126
  118. package/src/mcp/index.ts +0 -61
  119. package/src/mcp/resources/config-current.ts +0 -19
  120. package/src/mcp/resources/index.ts +0 -11
  121. package/src/mcp/resources/sessions-active.ts +0 -19
  122. package/src/mcp/resources/sessions-all.ts +0 -19
  123. package/src/mcp/schemas/sessions.ts +0 -3
  124. package/src/mcp/server.ts +0 -17
  125. package/src/mcp/services/api-adapter.ts +0 -48
  126. package/src/mcp/services/data-source.ts +0 -22
  127. package/src/mcp/services/store-adapter.ts +0 -40
  128. package/src/mcp/tools/get-active-sessions.ts +0 -16
  129. package/src/mcp/tools/get-session-children.ts +0 -24
  130. package/src/mcp/tools/get-session-messages.ts +0 -24
  131. package/src/mcp/tools/get-session.ts +0 -23
  132. package/src/mcp/tools/index.ts +0 -15
  133. package/src/mcp/tools/list-sessions.ts +0 -16
  134. package/src/neo-blessed.d.ts +0 -4
  135. package/src/oc/index.ts +0 -302
  136. package/src/service/manager.ts +0 -284
  137. package/src/shared/config.ts +0 -478
  138. package/src/shared/ipc.ts +0 -80
  139. package/src/shared/logger.ts +0 -248
  140. package/src/shared/paths.ts +0 -73
  141. package/src/shared/platform.ts +0 -110
  142. package/src/shared/session-view.ts +0 -241
  143. package/src/shared/types.ts +0 -299
  144. package/src/shared/utils.ts +0 -339
  145. package/src/tui/app.ts +0 -920
  146. package/src/tui/client.ts +0 -149
  147. package/src/tui/index.ts +0 -217
  148. package/src/types.ts +0 -113
  149. package/src/usage/README.md +0 -323
  150. package/src/usage/api.ts +0 -99
  151. package/src/usage/client.ts +0 -3
  152. package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
  153. package/src/usage/clients/anthropic-web-usage.ts +0 -245
  154. package/src/usage/clients/claude-cli-probe.ts +0 -187
  155. package/src/usage/clients/codex-oauth-usage.ts +0 -113
  156. package/src/usage/clients/opencode-go-usage.ts +0 -441
  157. package/src/usage/index.ts +0 -20
  158. package/src/usage/mappers.ts +0 -273
  159. package/src/usage/planner.ts +0 -54
  160. package/src/usage/providers/claude.ts +0 -113
  161. package/src/usage/providers/codex.ts +0 -34
  162. package/src/usage/providers/opencode-go.ts +0 -37
  163. package/src/usage/resolver.ts +0 -4
  164. package/src/usage/resolvers/browser-cookies.ts +0 -470
  165. package/src/usage/resolvers/claude-auth.ts +0 -108
  166. package/src/usage/resolvers/claude-cli.ts +0 -24
  167. package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
  168. package/src/usage/resolvers/claude-web.ts +0 -16
  169. package/src/usage/resolvers/codex-auth.ts +0 -117
  170. package/src/usage/resolvers/opencode-go-web.ts +0 -14
  171. package/src/usage/types.ts +0 -190
@@ -1,72 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- const SessionStateSchema = z.enum(["working", "idle", "waiting-input", "error", "shell", "unknown"]);
4
- const SessionToolSchema = z.enum(["opencode", "claude", "codex", "copilotcli", "shell", "unknown"]);
5
-
6
- const ProjectInfoSchema = z.object({
7
- displayName: z.string(),
8
- compactLabel: z.string(),
9
- branch: z.string().optional(),
10
- isGitRepository: z.boolean().optional(),
11
- sourcePath: z.string().optional(),
12
- gitRootPath: z.string().optional(),
13
- repositoryIdentityPath: z.string().optional(),
14
- });
15
-
16
- const TmuxInfoSchema = z.object({
17
- session: z.string(),
18
- window: z.string(),
19
- pane: z.string(),
20
- });
21
-
22
- export const SessionListItemSchema = z.object({
23
- id: z.string(),
24
- source: z.enum(["tmux", "standalone"]),
25
- tool: SessionToolSchema,
26
- state: SessionStateSchema,
27
- summary: z.string(),
28
- lastActivity: z.number(),
29
- stableForMs: z.number().optional(),
30
- hasAttention: z.boolean().optional(),
31
- detectionSource: z.string().optional(),
32
- parentSessionId: z.string().optional(),
33
- project: ProjectInfoSchema.optional(),
34
- tmux: TmuxInfoSchema.optional(),
35
- currentPath: z.string().optional(),
36
- activeTool: z.string().optional(),
37
- childCount: z.number().optional(),
38
- activeChildCount: z.number().optional(),
39
- });
40
-
41
- export const SessionTreeItemSchema = SessionListItemSchema.extend({
42
- children: z.array(SessionListItemSchema),
43
- });
44
-
45
- export const SessionViewCountsSchema = z.object({
46
- total: z.number(),
47
- visible: z.number(),
48
- rootsVisible: z.number(),
49
- childrenVisible: z.number(),
50
- byState: z.record(z.string(), z.number()),
51
- byTool: z.record(z.string(), z.number()),
52
- });
53
-
54
- export const SessionsResponseSchema = z.object({
55
- sessions: z.array(SessionTreeItemSchema),
56
- counts: SessionViewCountsSchema,
57
- generatedAt: z.number(),
58
- });
59
-
60
- export const SessionDetailResponseSchema = z.object({
61
- session: z.record(z.string(), z.unknown()).openapi({ description: "Full SessionModel" }),
62
- });
63
-
64
- export const SessionChildrenResponseSchema = z.object({
65
- sessionId: z.string(),
66
- children: z.array(SessionListItemSchema),
67
- });
68
-
69
- export const SessionMessagesResponseSchema = z.object({
70
- sessionId: z.string(),
71
- messages: z.array(z.unknown()),
72
- });
@@ -1,50 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- export const UsageWindowSchema = z.object({
4
- id: z.string(),
5
- title: z.string(),
6
- usedPercent: z.number().min(0).max(100),
7
- used: z.number().optional(),
8
- limit: z.number().optional(),
9
- windowMinutes: z.number().optional(),
10
- resetsAt: z.string().datetime().nullable().optional(),
11
- resetInSec: z.number().optional(),
12
- });
13
-
14
- export const ProviderCostSnapshotSchema = z.object({
15
- used: z.number(),
16
- limit: z.number(),
17
- currency: z.string(),
18
- period: z.string(),
19
- resetsAt: z.string().datetime().nullable().optional(),
20
- updatedAt: z.string().datetime(),
21
- });
22
-
23
- export const UsageProviderEntrySchema = z.object({
24
- provider: z.string(),
25
- accountId: z.string().nullable(),
26
- available: z.boolean(),
27
- status: z.enum(["ok", "error", "unavailable", "pending"]),
28
- metadata: z
29
- .object({
30
- dataSource: z.string().optional(),
31
- accountEmail: z.string().nullable().optional(),
32
- accountOrganization: z.string().nullable().optional(),
33
- subscriptionType: z.string().nullable().optional(),
34
- rateLimitTier: z.string().nullable().optional(),
35
- workspaceId: z.string().nullable().optional(),
36
- workspaceName: z.string().nullable().optional(),
37
- })
38
- .optional(),
39
- errors: z.array(z.string()).optional(),
40
- windows: z.array(UsageWindowSchema).optional(),
41
- cost: ProviderCostSnapshotSchema.nullable().optional(),
42
- updatedAt: z.string().datetime().optional(),
43
- });
44
-
45
- export const UsageStateSchema = z.object({
46
- providers: z.array(UsageProviderEntrySchema),
47
- lastRefreshedAt: z.number(),
48
- });
49
-
50
- export const UsageResponseSchema = UsageStateSchema;
@@ -1,32 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- // Minimal validation: only 'sessionId' is essential. All other fields are optional
4
- // and extra fields pass through transparently (.passthrough()) so future changes
5
- // to the Claude hook format don't cause unexpected rejections.
6
- export const ClaudeWebhookBodySchema = z
7
- .object({
8
- sessionId: z.string().openapi({ example: "abc-123-def" }),
9
- eventType: z.string().optional().openapi({ example: "PreToolUse" }),
10
- eventTime: z.number().optional(),
11
- hookSeq: z.number().optional(),
12
- state: z.enum(["working", "idle", "waiting-input", "error", "shell", "unknown"]).optional(),
13
- hasAttention: z.boolean().optional(),
14
- attentionKind: z.enum(["permission", "question", "turn", "none"]).optional(),
15
- summary: z.string().optional(),
16
- activeTool: z.string().optional(),
17
- toolName: z.string().optional(),
18
- toolInputSummary: z.string().optional(),
19
- toolResultStatus: z.enum(["started", "completed", "error", "blocked", "denied", "unknown"]).optional(),
20
- permissionMode: z.string().optional(),
21
- startSource: z.string().optional(),
22
- endReason: z.string().optional(),
23
- errorType: z.string().optional(),
24
- agentId: z.string().optional(),
25
- agentType: z.string().optional(),
26
- transcriptPath: z.string().optional(),
27
- currentPath: z.string().optional(),
28
- lastActivity: z.number().optional(),
29
- parentSessionId: z.string().optional(),
30
- detectionSource: z.literal("claude-hook").optional(),
31
- })
32
- .passthrough();
@@ -1,34 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- // Minimal validation: only 'sessionId' is essential. All other fields are optional
4
- // and extra fields pass through transparently (.passthrough()) so future changes
5
- // to the Codex hook format don't cause unexpected rejections.
6
- export const CodexWebhookBodySchema = z
7
- .object({
8
- sessionId: z.string().openapi({ example: "abc-123-def" }),
9
- eventType: z.string().optional().openapi({ example: "PreToolUse" }),
10
- eventTime: z.number().optional(),
11
- hookSeq: z.number().optional(),
12
- state: z.enum(["working", "idle", "waiting-input", "error", "shell", "unknown"]).optional(),
13
- hasAttention: z.boolean().optional(),
14
- attentionKind: z.enum(["permission", "question", "turn", "none"]).optional(),
15
- summary: z.string().optional(),
16
- activeTool: z.string().optional(),
17
- model: z.string().optional(),
18
- toolName: z.string().optional(),
19
- toolInputSummary: z.string().optional(),
20
- toolResultStatus: z.enum(["started", "completed", "error", "blocked", "denied", "unknown"]).optional(),
21
- permissionMode: z.string().optional(),
22
- startSource: z.string().optional(),
23
- endReason: z.string().optional(),
24
- transcriptPath: z.string().optional(),
25
- currentPath: z.string().optional(),
26
- lastActivity: z.number().optional(),
27
- parentSessionId: z.string().optional(),
28
- forkedFromId: z.string().optional(),
29
- turnId: z.string().optional(),
30
- agentId: z.string().optional(),
31
- agentType: z.string().optional(),
32
- detectionSource: z.literal("codex-hook").optional(),
33
- })
34
- .passthrough();
@@ -1,29 +0,0 @@
1
- import { z } from "@hono/zod-openapi";
2
-
3
- // Minimal validation: only 'event' is essential. All other fields are optional
4
- // and extra fields pass through transparently (.passthrough()) so future changes
5
- // to the OpenCode hook format don't cause unexpected rejections.
6
- export const OpencodeWebhookBodySchema = z
7
- .object({
8
- event: z.string().openapi({ example: "session.status" }),
9
- sessionID: z.string().optional().openapi({ example: "ses_abc123" }),
10
- directory: z.string().optional().openapi({ example: "/home/user/project" }),
11
- title: z.string().optional().openapi({ example: "My session" }),
12
- permissionID: z.string().optional(),
13
- info: z
14
- .object({
15
- id: z.string().optional(),
16
- title: z.string().optional(),
17
- directory: z.string().optional(),
18
- parentID: z.string().optional(),
19
- time: z
20
- .object({
21
- created: z.number().optional(),
22
- updated: z.number().optional(),
23
- })
24
- .optional(),
25
- })
26
- .optional(),
27
- status: z.object({ type: z.string().optional() }).optional(),
28
- })
29
- .passthrough();
@@ -1,9 +0,0 @@
1
- export const STATE_BEARING_EVENTS = new Set([
2
- "SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest",
3
- "PermissionDenied", "PostToolUse", "PostToolUseFailure", "PostToolBatch",
4
- "Stop", "StopFailure", "SubagentStart", "SubagentStop", "SessionEnd",
5
- ]);
6
-
7
- export function isMetadataOnlyEvent(eventType: string): boolean {
8
- return !STATE_BEARING_EVENTS.has(eventType);
9
- }
@@ -1,8 +0,0 @@
1
- export const STATE_BEARING_EVENTS = new Set([
2
- "SessionStart", "UserPromptSubmit", "PreToolUse", "PermissionRequest",
3
- "PostToolUse", "SubagentStart", "SubagentStop", "Stop",
4
- ]);
5
-
6
- export function isMetadataOnlyEvent(eventType: string): boolean {
7
- return !STATE_BEARING_EVENTS.has(eventType);
8
- }
@@ -1,73 +0,0 @@
1
- import type { SessionModel } from "../../shared/types";
2
- import type { OpencodeWebhookPayload, HookStateResult } from "../models/opencode";
3
-
4
- export function mapHookState(
5
- event: string,
6
- payload: OpencodeWebhookPayload,
7
- existing?: SessionModel,
8
- ): HookStateResult {
9
- const prevCount = (existing?.opencode?.pendingAttentionCount ?? 0);
10
- let nextCount = prevCount;
11
-
12
- if (event === "permission.updated" || event === "permission.asked" || event === "question.asked") {
13
- nextCount = prevCount + 1;
14
- } else if (event === "permission.replied" || event === "question.replied") {
15
- nextCount = Math.max(0, prevCount - 1);
16
- }
17
-
18
- const hasAttention = nextCount > 0;
19
-
20
- if (event === "session.error") {
21
- return { state: "error", hasAttention: false, pendingAttentionCount: 0 };
22
- }
23
-
24
- if (hasAttention) {
25
- return { state: "waiting-input", hasAttention: true, pendingAttentionCount: nextCount };
26
- }
27
-
28
- if (event === "session.status" && payload.status?.type) {
29
- return {
30
- state: (payload.status.type === "busy" || payload.status.type === "retry") ? "working" : "idle",
31
- hasAttention: false,
32
- pendingAttentionCount: nextCount,
33
- };
34
- }
35
-
36
- if (event === "session.idle") {
37
- return { state: "idle", hasAttention: false, pendingAttentionCount: nextCount };
38
- }
39
-
40
- if (event === "permission.replied" || event === "question.replied") {
41
- return { state: existing?.state === "waiting-input" ? "idle" : (existing?.state ?? "idle"), hasAttention: false, pendingAttentionCount: nextCount };
42
- }
43
-
44
- return {
45
- state: existing?.state ?? "idle",
46
- hasAttention,
47
- pendingAttentionCount: nextCount,
48
- };
49
- }
50
-
51
- export function extractSessionParentId(
52
- event: string,
53
- payload: OpencodeWebhookPayload,
54
- existing?: SessionModel,
55
- ): string | undefined {
56
- if (event !== "session.created" && event !== "session.updated") {
57
- return existing?.parentSessionId;
58
- }
59
-
60
- const parentID = payload.info?.parentID;
61
- if (!parentID) {
62
- return existing?.parentSessionId;
63
- }
64
-
65
- // Only treat real session ids as parent sessions. Other event payloads may
66
- // include message-part ancestry such as msg_* ids, which would incorrectly
67
- // reclassify a root session as a hidden child row.
68
- if (!parentID.startsWith("ses_")) {
69
- return existing?.parentSessionId;
70
- }
71
-
72
- return `opencode:${parentID}`;
73
- }
package/src/classifier.ts DELETED
@@ -1,206 +0,0 @@
1
- import { detectTool, getPreviewMatchPositions, previewSignals } from "./detect";
2
- import type {
3
- DetectionReason,
4
- EnrichedPaneRecord,
5
- PaneModel,
6
- PaneSample,
7
- PaneState,
8
- ProcessInfo,
9
- SnapshotHistory,
10
- ToolKind,
11
- } from "./types";
12
- import { cleanSummaryText, compactWhitespace, normalizeCommandName, pickCompletionTail, pickMeaningfulTail, pickQuestionTail, pickSummaryTail } from "./shared/utils";
13
-
14
- const DEBOUNCE_THRESHOLD = 2;
15
-
16
- export function classifyPane(input: {
17
- pane: EnrichedPaneRecord;
18
- processTree: ProcessInfo[];
19
- sample: PaneSample | null;
20
- history?: SnapshotHistory;
21
- now: number;
22
- }): { pane: PaneModel; history: SnapshotHistory } {
23
- const { pane, processTree, sample, history, now } = input;
24
- const detectedTool = detectTool(pane.currentCommand, processTree);
25
- const rawState = detectState(detectedTool.tool, pane.currentCommand, processTree, sample);
26
-
27
- const fingerprint = [
28
- detectedTool.tool,
29
- rawState.state,
30
- processTree.map((process) => `${process.pid}:${process.comm}`).join(","),
31
- sample?.preview ?? "",
32
- ].join("|");
33
-
34
- const lastChangedAt = history?.lastChangedAt ?? now;
35
- const stableCount = history?.lastFingerprint === fingerprint ? (history.stableCount + 1) : 1;
36
-
37
- let tool: ToolKind = detectedTool.tool;
38
- let state: PaneState = rawState.state;
39
- let stateReasons = rawState.reasons;
40
- const shouldBypassDebounce = rawState.state === "working"
41
- && rawState.reasons.some((reason) => (
42
- reason.label === "activity keywords"
43
- || reason.label === "opencode footer activity"
44
- || reason.label === "active agent signals"
45
- ));
46
-
47
- if (history && stableCount < DEBOUNCE_THRESHOLD && !shouldBypassDebounce) {
48
- tool = history.tool;
49
- state = history.state;
50
- stateReasons = [
51
- {
52
- label: "debounced previous state",
53
- detail: `holding ${history.state} until new signal stabilizes`,
54
- },
55
- ...rawState.reasons,
56
- ];
57
- }
58
-
59
- const historyRecord: SnapshotHistory = {
60
- tool,
61
- state,
62
- lastFingerprint: fingerprint,
63
- lastChangedAt: ((stableCount === DEBOUNCE_THRESHOLD && history?.state !== state) || (shouldBypassDebounce && stableCount === 1)) ? now : lastChangedAt,
64
- stableCount,
65
- };
66
-
67
- const processChain = processTree.map((process) => compactWhitespace(process.comm || normalizeCommandName(process.command)));
68
- const summary = summarizeState(state, stateReasons, sample, processChain, pane.currentCommand);
69
-
70
- return {
71
- pane: {
72
- ...pane,
73
- processTree,
74
- processChain,
75
- sample,
76
- tool,
77
- state,
78
- toolReasons: detectedTool.reasons,
79
- stateReasons,
80
- summary,
81
- lastChangedAt: historyRecord.lastChangedAt,
82
- stableForMs: Math.max(0, now - historyRecord.lastChangedAt),
83
- hasAttention: state === "waiting-input",
84
- },
85
- history: historyRecord,
86
- };
87
- }
88
-
89
- export function detectState(tool: ToolKind, currentCommand: string, processTree: ProcessInfo[], sample: PaneSample | null): { state: PaneState; reasons: DetectionReason[] } {
90
- const reasons: DetectionReason[] = [];
91
- const promptSignals = previewSignals(sample);
92
- const previewMatches = getPreviewMatchPositions(sample);
93
- const processStates = processTree.map((process) => process.state).join(",");
94
- const commands = [currentCommand, ...processTree.map((process) => process.command)].join(" ");
95
- const meaningfulPreview = sample ? pickMeaningfulTail(sample.lines, sample.preview) : "";
96
-
97
- if (tool === "shell") {
98
- reasons.push({ label: "shell pane", detail: compactWhitespace(currentCommand || processTree[0]?.command || "shell") });
99
- return { state: "shell", reasons };
100
- }
101
-
102
- if (tool === "unknown") {
103
- reasons.push({ label: "insufficient classification signals", detail: compactWhitespace(commands) || "unknown workload" });
104
- return { state: "unknown", reasons };
105
- }
106
-
107
- const activityReason = promptSignals.find((reason) => reason.label === "activity keywords");
108
- const footerReason = promptSignals.find((reason) => reason.label === "opencode footer activity");
109
- const waitingReason = promptSignals.find((reason) => {
110
- if (reason.label !== "input prompt keywords" && reason.label !== "prompt-like tail") {
111
- return false;
112
- }
113
-
114
- return !/^>\s*implement\s*\{feature\}/i.test(reason.detail);
115
- });
116
- const completionReason = promptSignals.find((reason) => reason.label === "completion keywords");
117
- const activeKeywords = /(thinking|generating|executing|applying|running|streaming|analyzing|diff|patch|searching|reading)/i;
118
- const strongWorkingSignal = Boolean(
119
- (activityReason && /(working\s*\(|background terminal running|waited for background terminal|created\s+\d+)/i.test(activityReason.detail))
120
- || (tool === "opencode" && footerReason),
121
- );
122
- const hasRunnableProcessSignal = processTree.some((process) => /[RU]/.test(process.state));
123
- const hasActiveProcessSignal = hasRunnableProcessSignal;
124
- const hasFreshActivitySignal = Boolean(
125
- previewMatches.activityIndex >= Math.max(previewMatches.promptIndex, previewMatches.completionIndex)
126
- && (
127
- (activityReason && (cleanSummaryText(activityReason.detail) === meaningfulPreview || activeKeywords.test(meaningfulPreview)))
128
- || (tool === "opencode" && footerReason)
129
- ),
130
- );
131
-
132
- if (completionReason && !hasActiveProcessSignal && previewMatches.completionIndex >= previewMatches.activityIndex) {
133
- reasons.push({
134
- label: "completed work detected",
135
- detail: completionReason.detail,
136
- });
137
- return { state: waitingReason ? "waiting-input" : "idle", reasons: waitingReason ? [...reasons, waitingReason] : reasons };
138
- }
139
-
140
- if (strongWorkingSignal || hasFreshActivitySignal) {
141
- if (footerReason && tool === "opencode") {
142
- reasons.push(footerReason);
143
- } else if (activityReason) {
144
- reasons.push(activityReason);
145
- }
146
- return { state: "working", reasons };
147
- }
148
-
149
- if (waitingReason) {
150
- reasons.push(waitingReason);
151
- return { state: "waiting-input", reasons };
152
- }
153
-
154
- if (hasActiveProcessSignal) {
155
- reasons.push(footerReason ?? activityReason ?? { label: "active agent signals", detail: meaningfulPreview || processStates || compactWhitespace(commands) });
156
- return { state: "working", reasons };
157
- }
158
-
159
- reasons.push({ label: "agent present without active signals", detail: meaningfulPreview || compactWhitespace(commands) || tool });
160
- return { state: "idle", reasons };
161
- }
162
-
163
- function summarizeState(
164
- state: PaneState,
165
- stateReasons: DetectionReason[],
166
- sample: PaneSample | null,
167
- processChain: string[],
168
- currentCommand: string,
169
- ): string {
170
- const primaryReason = stateReasons.find((reason) => reason.label !== "debounced previous state")?.detail;
171
- const hasCompletionReason = stateReasons.some((reason) => reason.label === "completed work detected" || reason.label === "completion keywords");
172
- const preferredSummary = state === "waiting-input"
173
- ? pickQuestionTail(sample?.lines ?? [], primaryReason ?? "")
174
- : hasCompletionReason
175
- ? pickCompletionTail(sample?.lines ?? [], primaryReason ?? "")
176
- : pickSummaryTail(sample?.lines ?? [], primaryReason ?? "");
177
-
178
- if (primaryReason && primaryReason.length > 0 && primaryReason === preferredSummary) {
179
- return primaryReason;
180
- }
181
-
182
- if (sample?.lines.length) {
183
- const meaningful = state === "waiting-input"
184
- ? pickQuestionTail(sample.lines, sample.preview)
185
- : hasCompletionReason
186
- ? pickCompletionTail(sample.lines, sample.preview)
187
- : pickSummaryTail(sample.lines, sample.preview);
188
- if (meaningful) {
189
- return meaningful;
190
- }
191
- }
192
-
193
- if (sample?.preview) {
194
- return sample.preview;
195
- }
196
-
197
- if (processChain.length > 0) {
198
- return processChain.join(" -> ");
199
- }
200
-
201
- if (currentCommand) {
202
- return currentCommand;
203
- }
204
-
205
- return state;
206
- }