@wangyaoshen/remux 0.3.8-dev.a8ceb0c
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/.github/ISSUE_TEMPLATE/bug_report.md +47 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +38 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +65 -0
- package/.github/workflows/deploy.yml +65 -0
- package/.github/workflows/publish.yml +312 -0
- package/.github/workflows/release-please.yml +21 -0
- package/.gitmodules +3 -0
- package/.nvmrc +1 -0
- package/.release-please-manifest.json +3 -0
- package/CLAUDE.md +104 -0
- package/Dockerfile +23 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/apps/ios/Config/signing.xcconfig +4 -0
- package/apps/ios/Package.swift +26 -0
- package/apps/ios/Remux.xcodeproj/project.pbxproj +477 -0
- package/apps/ios/Remux.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/Contents.json +23 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_120x120.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_152x152.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_167x167.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_180x180.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_20x20.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_29x29.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_40x40.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_58x58.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_60x60.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_76x76.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_80x80.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/AppIcon.appiconset/icon_87x87.png +0 -0
- package/apps/ios/Sources/Remux/Assets.xcassets/Contents.json +6 -0
- package/apps/ios/Sources/Remux/Extensions/FaceIDManager.swift +29 -0
- package/apps/ios/Sources/Remux/Extensions/InspectCache.swift +66 -0
- package/apps/ios/Sources/Remux/MainTabView.swift +32 -0
- package/apps/ios/Sources/Remux/Remux.entitlements +8 -0
- package/apps/ios/Sources/Remux/RemuxiOSApp.swift +14 -0
- package/apps/ios/Sources/Remux/RootView.swift +130 -0
- package/apps/ios/Sources/Remux/Views/Control/ControlView.swift +102 -0
- package/apps/ios/Sources/Remux/Views/Inspect/InspectView.swift +98 -0
- package/apps/ios/Sources/Remux/Views/Live/LiveTerminalView.swift +132 -0
- package/apps/ios/Sources/Remux/Views/Now/NowView.swift +173 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/ManualConnectView.swift +55 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/OnboardingView.swift +70 -0
- package/apps/ios/Sources/Remux/Views/Onboarding/QRScannerView.swift +92 -0
- package/apps/ios/Sources/Remux/Views/Settings/MeView.swift +136 -0
- package/apps/macos/Package.swift +37 -0
- package/apps/macos/Resources/shell-integration/bash/bash-preexec.sh +382 -0
- package/apps/macos/Resources/shell-integration/bash/ghostty.bash +315 -0
- package/apps/macos/Resources/shell-integration/elvish/lib/ghostty-integration.elv +191 -0
- package/apps/macos/Resources/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +246 -0
- package/apps/macos/Resources/shell-integration/nushell/vendor/autoload/ghostty.nu +110 -0
- package/apps/macos/Resources/shell-integration/zsh/.zshenv +61 -0
- package/apps/macos/Resources/shell-integration/zsh/ghostty-integration +458 -0
- package/apps/macos/Resources/terminfo/67/ghostty +0 -0
- package/apps/macos/Resources/terminfo/78/xterm-ghostty +0 -0
- package/apps/macos/Sources/Remux/AppDelegate.swift +257 -0
- package/apps/macos/Sources/Remux/CrashReporter.swift +210 -0
- package/apps/macos/Sources/Remux/FinderIntegration.swift +117 -0
- package/apps/macos/Sources/Remux/GhosttyConfig.swift +311 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutAction.swift +115 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/ShortcutSettingsView.swift +271 -0
- package/apps/macos/Sources/Remux/KeyboardShortcuts/StoredShortcut.swift +149 -0
- package/apps/macos/Sources/Remux/MainContentView.swift +308 -0
- package/apps/macos/Sources/Remux/MenuBarManager.swift +275 -0
- package/apps/macos/Sources/Remux/NotificationManager.swift +145 -0
- package/apps/macos/Sources/Remux/PortScanner.swift +152 -0
- package/apps/macos/Sources/Remux/RemuxApp.swift +13 -0
- package/apps/macos/Sources/Remux/SSHDetector.swift +151 -0
- package/apps/macos/Sources/Remux/SessionPersistence.swift +226 -0
- package/apps/macos/Sources/Remux/SocketController.swift +258 -0
- package/apps/macos/Sources/Remux/UpdateChecker.swift +152 -0
- package/apps/macos/Sources/Remux/Views/CommandPalette.swift +198 -0
- package/apps/macos/Sources/Remux/Views/ConnectionView.swift +84 -0
- package/apps/macos/Sources/Remux/Views/InspectView.swift +127 -0
- package/apps/macos/Sources/Remux/Views/SettingsView.swift +77 -0
- package/apps/macos/Sources/Remux/Views/Sidebar/SidebarView.swift +410 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/BrowserPanel.swift +193 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/MarkdownPanel.swift +277 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/PanelProtocol.swift +14 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitNode.swift +149 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/SplitView.swift +234 -0
- package/apps/macos/Sources/Remux/Views/SplitTree/TerminalPanel.swift +26 -0
- package/apps/macos/Sources/Remux/Views/TabBarView.swift +94 -0
- package/apps/macos/Sources/Remux/Views/Terminal/ClipboardHelper.swift +101 -0
- package/apps/macos/Sources/Remux/Views/Terminal/CopyModeOverlay.swift +325 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeTerminalView.swift +39 -0
- package/apps/macos/Sources/Remux/Views/Terminal/GhosttyNativeView.swift +559 -0
- package/apps/macos/Sources/Remux/Views/Terminal/SurfaceSearchOverlay.swift +109 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalContainerView.swift +95 -0
- package/apps/macos/Sources/Remux/Views/Terminal/TerminalRelay.swift +117 -0
- package/build.mjs +33 -0
- package/native/android/DecodeGoldenPayloads.kt +487 -0
- package/native/android/ProtocolModels.kt +188 -0
- package/native/ios/DecodeGoldenPayloads.swift +711 -0
- package/native/ios/ProtocolModels.swift +200 -0
- package/package.json +45 -0
- package/packages/RemuxKit/Package.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Device/DeviceManager.swift +27 -0
- package/packages/RemuxKit/Sources/RemuxKit/Models/ProtocolModels.swift +206 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/MessageRouter.swift +108 -0
- package/packages/RemuxKit/Sources/RemuxKit/Networking/RemuxConnection.swift +395 -0
- package/packages/RemuxKit/Sources/RemuxKit/State/RemuxState.swift +188 -0
- package/packages/RemuxKit/Sources/RemuxKit/Storage/KeychainStore.swift +142 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyBridge.swift +145 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/GhosttyTerminalView.swift +35 -0
- package/packages/RemuxKit/Sources/RemuxKit/Terminal/Resources/ghostty-terminal.html +91 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ConnectionIntegrationTest.swift +74 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/KeychainStoreTests.swift +81 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/ProtocolModelsTests.swift +179 -0
- package/packages/RemuxKit/Tests/RemuxKitTests/RemuxStateTests.swift +62 -0
- package/playwright.config.ts +17 -0
- package/pnpm-lock.yaml +1588 -0
- package/pty-daemon.js +303 -0
- package/release-please-config.json +14 -0
- package/scripts/auto-deploy.sh +46 -0
- package/scripts/build-dmg.sh +121 -0
- package/scripts/build-ghostty-kit.sh +43 -0
- package/scripts/check-active-terminology.mjs +132 -0
- package/scripts/setup-ci-secrets.sh +80 -0
- package/scripts/sync-ghostty-web.sh +28 -0
- package/scripts/upload-testflight.sh +100 -0
- package/server.js +7074 -0
- package/src/adapters/agent-events.ts +246 -0
- package/src/adapters/claude-code.ts +158 -0
- package/src/adapters/codex.ts +210 -0
- package/src/adapters/generic-shell.ts +58 -0
- package/src/adapters/index.ts +15 -0
- package/src/adapters/registry.ts +99 -0
- package/src/adapters/types.ts +41 -0
- package/src/auth.ts +174 -0
- package/src/e2ee.ts +236 -0
- package/src/git-service.ts +168 -0
- package/src/message-buffer.ts +137 -0
- package/src/pty-daemon.ts +357 -0
- package/src/push.ts +127 -0
- package/src/renderers.ts +455 -0
- package/src/server.ts +2407 -0
- package/src/service.ts +226 -0
- package/src/session.ts +978 -0
- package/src/store.ts +1422 -0
- package/src/team.ts +123 -0
- package/src/tunnel.ts +126 -0
- package/src/types.d.ts +50 -0
- package/src/vt-tracker.ts +188 -0
- package/src/workspace-head.ts +144 -0
- package/src/workspace.ts +153 -0
- package/src/ws-handler.ts +1526 -0
- package/start.ps1 +83 -0
- package/tests/adapters.test.js +171 -0
- package/tests/auth.test.js +243 -0
- package/tests/codex-adapter.test.js +535 -0
- package/tests/durable-stream.test.js +153 -0
- package/tests/e2e/app.spec.js +530 -0
- package/tests/e2ee.test.js +325 -0
- package/tests/message-buffer.test.js +245 -0
- package/tests/message-routing.test.js +305 -0
- package/tests/pty-daemon.test.js +346 -0
- package/tests/push.test.js +281 -0
- package/tests/renderers.test.js +391 -0
- package/tests/search-shell.test.js +499 -0
- package/tests/server.test.js +882 -0
- package/tests/service.test.js +267 -0
- package/tests/store.test.js +369 -0
- package/tests/tunnel.test.js +67 -0
- package/tests/workspace-head.test.js +116 -0
- package/tests/workspace.test.js +417 -0
- package/tsconfig.backend.json +11 -0
- package/tsconfig.json +15 -0
- package/tui/client/client_test.go +125 -0
- package/tui/client/connection.go +342 -0
- package/tui/client/host_manager.go +141 -0
- package/tui/config/cache.go +81 -0
- package/tui/config/config.go +53 -0
- package/tui/config/config_test.go +89 -0
- package/tui/go.mod +32 -0
- package/tui/go.sum +50 -0
- package/tui/main.go +261 -0
- package/tui/tests/integration_test.go +283 -0
- package/tui/ui/model.go +310 -0
- package/vitest.config.js +10 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
// ── CodexAdapter terminal pattern detection ───────────────────────
|
|
4
|
+
|
|
5
|
+
describe("CodexAdapter terminal patterns", () => {
|
|
6
|
+
// Helper: simulates onTerminalData by applying the same regex/includes logic
|
|
7
|
+
// as the CodexAdapter without importing the full module (avoids node-pty dep)
|
|
8
|
+
function detectState(data) {
|
|
9
|
+
// Running: thinking/working/spinner
|
|
10
|
+
if (
|
|
11
|
+
data.includes("Thinking...") ||
|
|
12
|
+
data.includes("Working...") ||
|
|
13
|
+
data.includes("⠋") ||
|
|
14
|
+
data.includes("⠙") ||
|
|
15
|
+
data.includes("⠹")
|
|
16
|
+
) {
|
|
17
|
+
return "running";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Running: tool use
|
|
21
|
+
if (
|
|
22
|
+
data.includes("Reading file:") ||
|
|
23
|
+
data.includes("Writing file:") ||
|
|
24
|
+
data.includes("Editing file:") ||
|
|
25
|
+
data.includes("Running:") ||
|
|
26
|
+
data.includes("Patch:") ||
|
|
27
|
+
data.includes("[tool_use]")
|
|
28
|
+
) {
|
|
29
|
+
return "running";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Waiting approval
|
|
33
|
+
if (
|
|
34
|
+
data.includes("Approve?") ||
|
|
35
|
+
data.includes("[y/N]") ||
|
|
36
|
+
data.includes("[y/n]") ||
|
|
37
|
+
data.includes("Allow this?") ||
|
|
38
|
+
data.includes("Run command?")
|
|
39
|
+
) {
|
|
40
|
+
return "waiting_approval";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Error (only if codex context)
|
|
44
|
+
if (data.includes("Error:") || data.includes("Failed:")) {
|
|
45
|
+
if (
|
|
46
|
+
data.includes("codex") ||
|
|
47
|
+
data.includes("Codex") ||
|
|
48
|
+
data.includes("codex>")
|
|
49
|
+
) {
|
|
50
|
+
return "error";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Idle: prompt return
|
|
55
|
+
if (data.includes("codex>") || data.includes("Done")) {
|
|
56
|
+
return "idle";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it("should detect running state from spinner characters", () => {
|
|
63
|
+
expect(detectState("⠋ Thinking...")).toBe("running");
|
|
64
|
+
expect(detectState("Working...")).toBe("running");
|
|
65
|
+
expect(detectState("⠙")).toBe("running");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should detect running state from tool use indicators", () => {
|
|
69
|
+
expect(detectState("Reading file: src/index.ts")).toBe("running");
|
|
70
|
+
expect(detectState("Writing file: output.json")).toBe("running");
|
|
71
|
+
expect(detectState("Editing file: config.yaml")).toBe("running");
|
|
72
|
+
expect(detectState("Running: npm test")).toBe("running");
|
|
73
|
+
expect(detectState("Patch: applied 3 hunks")).toBe("running");
|
|
74
|
+
expect(detectState("[tool_use] bash")).toBe("running");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should detect waiting_approval state", () => {
|
|
78
|
+
expect(detectState("Approve? [y/N]")).toBe("waiting_approval");
|
|
79
|
+
expect(detectState("Allow this? The command will modify files")).toBe(
|
|
80
|
+
"waiting_approval",
|
|
81
|
+
);
|
|
82
|
+
expect(detectState("Run command? [y/n]")).toBe("waiting_approval");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should detect error state only with codex context", () => {
|
|
86
|
+
expect(detectState("codex> Error: rate limit exceeded")).toBe("error");
|
|
87
|
+
expect(detectState("Codex Error: connection failed")).toBe("error");
|
|
88
|
+
// Generic errors without codex context should NOT trigger error state
|
|
89
|
+
expect(detectState("Error: file not found")).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should detect idle state from prompt return", () => {
|
|
93
|
+
expect(detectState("codex> ")).toBe("idle");
|
|
94
|
+
expect(detectState("Done")).toBe("idle");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should return null for unrecognized output", () => {
|
|
98
|
+
expect(detectState("normal terminal output")).toBeNull();
|
|
99
|
+
expect(detectState("ls -la")).toBeNull();
|
|
100
|
+
expect(detectState("")).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── Codex session event handling ──────────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe("Codex session events", () => {
|
|
107
|
+
function mapEventToState(event) {
|
|
108
|
+
const type = event.type;
|
|
109
|
+
if (
|
|
110
|
+
type === "item.created" ||
|
|
111
|
+
type === "tool_use" ||
|
|
112
|
+
type === "turn.started"
|
|
113
|
+
) {
|
|
114
|
+
return "running";
|
|
115
|
+
} else if (type === "turn.completed" || type === "done") {
|
|
116
|
+
return "idle";
|
|
117
|
+
} else if (type === "permission_request") {
|
|
118
|
+
return "waiting_approval";
|
|
119
|
+
} else if (type === "error") {
|
|
120
|
+
return "error";
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
it("should map item.created to running", () => {
|
|
126
|
+
expect(mapEventToState({ type: "item.created", item: {} })).toBe(
|
|
127
|
+
"running",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should map tool_use to running", () => {
|
|
132
|
+
expect(
|
|
133
|
+
mapEventToState({ type: "tool_use", name: "shell", input: {} }),
|
|
134
|
+
).toBe("running");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should map turn.started to running", () => {
|
|
138
|
+
expect(mapEventToState({ type: "turn.started" })).toBe("running");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should map turn.completed to idle", () => {
|
|
142
|
+
expect(mapEventToState({ type: "turn.completed" })).toBe("idle");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should map done to idle", () => {
|
|
146
|
+
expect(mapEventToState({ type: "done" })).toBe("idle");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should map permission_request to waiting_approval", () => {
|
|
150
|
+
expect(mapEventToState({ type: "permission_request" })).toBe(
|
|
151
|
+
"waiting_approval",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should map error to error", () => {
|
|
156
|
+
expect(mapEventToState({ type: "error", message: "fail" })).toBe("error");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── agent-events.ts: parseClaudeCodeEvent ─────────────────────────
|
|
161
|
+
|
|
162
|
+
describe("parseClaudeCodeEvent", () => {
|
|
163
|
+
// Inline parser logic to avoid importing ESM with node-pty transitive deps
|
|
164
|
+
function parseClaudeCodeEvent(event) {
|
|
165
|
+
const type = event.type;
|
|
166
|
+
if (!type) return null;
|
|
167
|
+
|
|
168
|
+
switch (type) {
|
|
169
|
+
case "assistant": {
|
|
170
|
+
const message = event.message;
|
|
171
|
+
const contentBlocks = message?.content ?? [];
|
|
172
|
+
const textParts = contentBlocks
|
|
173
|
+
.filter((b) => b.type === "text")
|
|
174
|
+
.map((b) => b.text);
|
|
175
|
+
return {
|
|
176
|
+
role: "assistant",
|
|
177
|
+
content: textParts.join("\n") || "",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
case "tool_use":
|
|
181
|
+
return {
|
|
182
|
+
role: "assistant",
|
|
183
|
+
content: "",
|
|
184
|
+
toolCalls: [
|
|
185
|
+
{
|
|
186
|
+
tool: event.name ?? "unknown",
|
|
187
|
+
args: event.input ?? {},
|
|
188
|
+
status: "running",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
case "tool_result":
|
|
193
|
+
return {
|
|
194
|
+
role: "assistant",
|
|
195
|
+
content: "",
|
|
196
|
+
toolCalls: [
|
|
197
|
+
{
|
|
198
|
+
tool: event.name ?? "unknown",
|
|
199
|
+
args: {},
|
|
200
|
+
status: "completed",
|
|
201
|
+
output:
|
|
202
|
+
typeof event.content === "string"
|
|
203
|
+
? event.content
|
|
204
|
+
: JSON.stringify(event.content ?? ""),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
case "permission_request":
|
|
209
|
+
return {
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: "",
|
|
212
|
+
approvals: [
|
|
213
|
+
{
|
|
214
|
+
id: event.id ?? "generated",
|
|
215
|
+
tool: event.tool ?? "unknown",
|
|
216
|
+
description: event.description ?? "",
|
|
217
|
+
status: "pending",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
case "result":
|
|
222
|
+
case "end_turn":
|
|
223
|
+
return {
|
|
224
|
+
role: "assistant",
|
|
225
|
+
content: typeof event.result === "string" ? event.result : "",
|
|
226
|
+
};
|
|
227
|
+
case "error":
|
|
228
|
+
return {
|
|
229
|
+
role: "assistant",
|
|
230
|
+
content: `Error: ${event.error ?? "unknown error"}`,
|
|
231
|
+
};
|
|
232
|
+
default:
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
it("should parse assistant message with text blocks", () => {
|
|
238
|
+
const result = parseClaudeCodeEvent({
|
|
239
|
+
type: "assistant",
|
|
240
|
+
message: {
|
|
241
|
+
content: [
|
|
242
|
+
{ type: "text", text: "Hello" },
|
|
243
|
+
{ type: "text", text: "World" },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
expect(result).not.toBeNull();
|
|
248
|
+
expect(result.role).toBe("assistant");
|
|
249
|
+
expect(result.content).toBe("Hello\nWorld");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("should parse tool_use event", () => {
|
|
253
|
+
const result = parseClaudeCodeEvent({
|
|
254
|
+
type: "tool_use",
|
|
255
|
+
name: "Bash",
|
|
256
|
+
input: { command: "ls -la" },
|
|
257
|
+
});
|
|
258
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
259
|
+
expect(result.toolCalls[0].tool).toBe("Bash");
|
|
260
|
+
expect(result.toolCalls[0].status).toBe("running");
|
|
261
|
+
expect(result.toolCalls[0].args.command).toBe("ls -la");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should parse tool_result event", () => {
|
|
265
|
+
const result = parseClaudeCodeEvent({
|
|
266
|
+
type: "tool_result",
|
|
267
|
+
name: "Bash",
|
|
268
|
+
content: "file1.txt\nfile2.txt",
|
|
269
|
+
});
|
|
270
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
271
|
+
expect(result.toolCalls[0].status).toBe("completed");
|
|
272
|
+
expect(result.toolCalls[0].output).toBe("file1.txt\nfile2.txt");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("should parse permission_request event", () => {
|
|
276
|
+
const result = parseClaudeCodeEvent({
|
|
277
|
+
type: "permission_request",
|
|
278
|
+
id: "req-1",
|
|
279
|
+
tool: "Bash",
|
|
280
|
+
description: "Run rm -rf /tmp/old",
|
|
281
|
+
});
|
|
282
|
+
expect(result.approvals).toHaveLength(1);
|
|
283
|
+
expect(result.approvals[0].tool).toBe("Bash");
|
|
284
|
+
expect(result.approvals[0].status).toBe("pending");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should parse result/end_turn events", () => {
|
|
288
|
+
const r1 = parseClaudeCodeEvent({
|
|
289
|
+
type: "result",
|
|
290
|
+
result: "Task completed",
|
|
291
|
+
});
|
|
292
|
+
expect(r1.content).toBe("Task completed");
|
|
293
|
+
|
|
294
|
+
const r2 = parseClaudeCodeEvent({ type: "end_turn" });
|
|
295
|
+
expect(r2.content).toBe("");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should parse error events", () => {
|
|
299
|
+
const result = parseClaudeCodeEvent({
|
|
300
|
+
type: "error",
|
|
301
|
+
error: "rate limit",
|
|
302
|
+
});
|
|
303
|
+
expect(result.content).toBe("Error: rate limit");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("should return null for unknown types", () => {
|
|
307
|
+
expect(parseClaudeCodeEvent({ type: "unknown_type" })).toBeNull();
|
|
308
|
+
expect(parseClaudeCodeEvent({})).toBeNull();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ── agent-events.ts: parseCodexEvent ──────────────────────────────
|
|
313
|
+
|
|
314
|
+
describe("parseCodexEvent", () => {
|
|
315
|
+
function parseCodexEvent(event) {
|
|
316
|
+
const type = event.type;
|
|
317
|
+
if (!type) return null;
|
|
318
|
+
|
|
319
|
+
switch (type) {
|
|
320
|
+
case "item.created": {
|
|
321
|
+
const item = event.item;
|
|
322
|
+
if (!item) return null;
|
|
323
|
+
const role = item.role === "user" ? "user" : "assistant";
|
|
324
|
+
const contentBlocks = item.content ?? [];
|
|
325
|
+
const textParts = contentBlocks
|
|
326
|
+
.filter((b) => b.type === "text" || b.type === "output_text")
|
|
327
|
+
.map((b) => b.text ?? b.output ?? "");
|
|
328
|
+
return {
|
|
329
|
+
role,
|
|
330
|
+
content: textParts.join("\n") || "",
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
case "tool_use":
|
|
334
|
+
return {
|
|
335
|
+
role: "assistant",
|
|
336
|
+
content: "",
|
|
337
|
+
toolCalls: [
|
|
338
|
+
{
|
|
339
|
+
tool: event.name ?? "unknown",
|
|
340
|
+
args: event.input ?? {},
|
|
341
|
+
status: "running",
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
case "tool_result":
|
|
346
|
+
return {
|
|
347
|
+
role: "assistant",
|
|
348
|
+
content: "",
|
|
349
|
+
toolCalls: [
|
|
350
|
+
{
|
|
351
|
+
tool: event.name ?? "unknown",
|
|
352
|
+
args: {},
|
|
353
|
+
status: "completed",
|
|
354
|
+
output:
|
|
355
|
+
typeof event.output === "string"
|
|
356
|
+
? event.output
|
|
357
|
+
: JSON.stringify(event.output ?? ""),
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
case "permission_request":
|
|
362
|
+
return {
|
|
363
|
+
role: "assistant",
|
|
364
|
+
content: "",
|
|
365
|
+
approvals: [
|
|
366
|
+
{
|
|
367
|
+
id: event.id ?? "generated",
|
|
368
|
+
tool: event.command ?? "unknown",
|
|
369
|
+
description: event.description ?? "",
|
|
370
|
+
status: "pending",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
};
|
|
374
|
+
case "turn.started":
|
|
375
|
+
case "turn.completed":
|
|
376
|
+
return { role: "assistant", content: "" };
|
|
377
|
+
case "error":
|
|
378
|
+
return {
|
|
379
|
+
role: "assistant",
|
|
380
|
+
content: `Error: ${event.message ?? "unknown error"}`,
|
|
381
|
+
};
|
|
382
|
+
default:
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
it("should parse item.created with assistant message", () => {
|
|
388
|
+
const result = parseCodexEvent({
|
|
389
|
+
type: "item.created",
|
|
390
|
+
item: {
|
|
391
|
+
role: "assistant",
|
|
392
|
+
content: [
|
|
393
|
+
{ type: "text", text: "I'll help you" },
|
|
394
|
+
{ type: "output_text", output: "with that" },
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
expect(result).not.toBeNull();
|
|
399
|
+
expect(result.role).toBe("assistant");
|
|
400
|
+
expect(result.content).toBe("I'll help you\nwith that");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should parse item.created with user message", () => {
|
|
404
|
+
const result = parseCodexEvent({
|
|
405
|
+
type: "item.created",
|
|
406
|
+
item: {
|
|
407
|
+
role: "user",
|
|
408
|
+
content: [{ type: "text", text: "fix the bug" }],
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
expect(result.role).toBe("user");
|
|
412
|
+
expect(result.content).toBe("fix the bug");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should return null for item.created without item", () => {
|
|
416
|
+
expect(parseCodexEvent({ type: "item.created" })).toBeNull();
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("should parse tool_use event", () => {
|
|
420
|
+
const result = parseCodexEvent({
|
|
421
|
+
type: "tool_use",
|
|
422
|
+
name: "shell",
|
|
423
|
+
input: { command: "npm test" },
|
|
424
|
+
});
|
|
425
|
+
expect(result.toolCalls).toHaveLength(1);
|
|
426
|
+
expect(result.toolCalls[0].tool).toBe("shell");
|
|
427
|
+
expect(result.toolCalls[0].status).toBe("running");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("should parse tool_result event", () => {
|
|
431
|
+
const result = parseCodexEvent({
|
|
432
|
+
type: "tool_result",
|
|
433
|
+
name: "shell",
|
|
434
|
+
output: "All tests passed",
|
|
435
|
+
});
|
|
436
|
+
expect(result.toolCalls[0].status).toBe("completed");
|
|
437
|
+
expect(result.toolCalls[0].output).toBe("All tests passed");
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("should parse permission_request event", () => {
|
|
441
|
+
const result = parseCodexEvent({
|
|
442
|
+
type: "permission_request",
|
|
443
|
+
id: "perm-1",
|
|
444
|
+
command: "rm -rf /tmp",
|
|
445
|
+
description: "Delete temp files",
|
|
446
|
+
});
|
|
447
|
+
expect(result.approvals).toHaveLength(1);
|
|
448
|
+
expect(result.approvals[0].tool).toBe("rm -rf /tmp");
|
|
449
|
+
expect(result.approvals[0].status).toBe("pending");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should parse turn lifecycle events", () => {
|
|
453
|
+
const started = parseCodexEvent({ type: "turn.started" });
|
|
454
|
+
expect(started.role).toBe("assistant");
|
|
455
|
+
|
|
456
|
+
const completed = parseCodexEvent({ type: "turn.completed" });
|
|
457
|
+
expect(completed.role).toBe("assistant");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("should parse error events", () => {
|
|
461
|
+
const result = parseCodexEvent({
|
|
462
|
+
type: "error",
|
|
463
|
+
message: "connection timeout",
|
|
464
|
+
});
|
|
465
|
+
expect(result.content).toBe("Error: connection timeout");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("should return null for unknown types", () => {
|
|
469
|
+
expect(parseCodexEvent({ type: "unknown_type" })).toBeNull();
|
|
470
|
+
expect(parseCodexEvent({})).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ── AgentSessionSummary generation ────────────────────────────────
|
|
475
|
+
|
|
476
|
+
describe("AgentSessionSummary generation", () => {
|
|
477
|
+
it("should build summary from adapter state", () => {
|
|
478
|
+
const adapterState = {
|
|
479
|
+
adapterId: "codex",
|
|
480
|
+
name: "OpenAI Codex",
|
|
481
|
+
mode: "passive",
|
|
482
|
+
capabilities: ["run-status", "conversation-events", "tool-use"],
|
|
483
|
+
currentState: "running",
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const summary = {
|
|
487
|
+
agentId: adapterState.adapterId,
|
|
488
|
+
agentName: adapterState.name,
|
|
489
|
+
state: adapterState.currentState,
|
|
490
|
+
currentTurn: undefined,
|
|
491
|
+
recentToolCalls: [],
|
|
492
|
+
pendingApprovals: [],
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
expect(summary.agentId).toBe("codex");
|
|
496
|
+
expect(summary.agentName).toBe("OpenAI Codex");
|
|
497
|
+
expect(summary.state).toBe("running");
|
|
498
|
+
expect(summary.recentToolCalls).toHaveLength(0);
|
|
499
|
+
expect(summary.pendingApprovals).toHaveLength(0);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("should handle multiple adapters", () => {
|
|
503
|
+
const states = [
|
|
504
|
+
{
|
|
505
|
+
adapterId: "generic-shell",
|
|
506
|
+
name: "Shell",
|
|
507
|
+
currentState: "idle",
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
adapterId: "claude-code",
|
|
511
|
+
name: "Claude Code",
|
|
512
|
+
currentState: "running",
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
adapterId: "codex",
|
|
516
|
+
name: "OpenAI Codex",
|
|
517
|
+
currentState: "waiting_approval",
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const summaries = states.map((s) => ({
|
|
522
|
+
agentId: s.adapterId,
|
|
523
|
+
agentName: s.name,
|
|
524
|
+
state: s.currentState,
|
|
525
|
+
currentTurn: undefined,
|
|
526
|
+
recentToolCalls: [],
|
|
527
|
+
pendingApprovals: [],
|
|
528
|
+
}));
|
|
529
|
+
|
|
530
|
+
expect(summaries).toHaveLength(3);
|
|
531
|
+
expect(summaries[0].state).toBe("idle");
|
|
532
|
+
expect(summaries[1].state).toBe("running");
|
|
533
|
+
expect(summaries[2].state).toBe("waiting_approval");
|
|
534
|
+
});
|
|
535
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for durable terminal stream — tab_stream_chunks, tab_snapshots, device_tab_cursors.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import {
|
|
7
|
+
_resetDbForTest,
|
|
8
|
+
closeDb,
|
|
9
|
+
saveStreamChunk,
|
|
10
|
+
getChunksSince,
|
|
11
|
+
saveTabSnapshot,
|
|
12
|
+
getLatestSnapshot,
|
|
13
|
+
updateDeviceCursor,
|
|
14
|
+
getDeviceCursor,
|
|
15
|
+
} from "../src/store.js";
|
|
16
|
+
|
|
17
|
+
describe("durable stream", () => {
|
|
18
|
+
let db;
|
|
19
|
+
|
|
20
|
+
beforeAll(() => {
|
|
21
|
+
db = new Database(":memory:");
|
|
22
|
+
db.pragma("journal_mode = WAL");
|
|
23
|
+
db.pragma("foreign_keys = ON");
|
|
24
|
+
_resetDbForTest(db);
|
|
25
|
+
|
|
26
|
+
// Create the stream tables
|
|
27
|
+
db.exec(`
|
|
28
|
+
CREATE TABLE IF NOT EXISTS tab_stream_chunks (
|
|
29
|
+
tab_id INTEGER NOT NULL,
|
|
30
|
+
seq_from INTEGER NOT NULL,
|
|
31
|
+
seq_to INTEGER NOT NULL,
|
|
32
|
+
created_at INTEGER NOT NULL,
|
|
33
|
+
data BLOB NOT NULL,
|
|
34
|
+
PRIMARY KEY (tab_id, seq_from)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
CREATE TABLE IF NOT EXISTS tab_snapshots (
|
|
38
|
+
tab_id INTEGER NOT NULL,
|
|
39
|
+
seq INTEGER NOT NULL,
|
|
40
|
+
cols INTEGER NOT NULL,
|
|
41
|
+
rows INTEGER NOT NULL,
|
|
42
|
+
snapshot BLOB NOT NULL,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
PRIMARY KEY (tab_id, seq)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS device_tab_cursors (
|
|
48
|
+
device_id TEXT NOT NULL,
|
|
49
|
+
tab_id INTEGER NOT NULL,
|
|
50
|
+
last_acked_seq INTEGER NOT NULL,
|
|
51
|
+
updated_at INTEGER NOT NULL,
|
|
52
|
+
PRIMARY KEY (device_id, tab_id)
|
|
53
|
+
);
|
|
54
|
+
`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(() => {
|
|
58
|
+
closeDb();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("tab_stream_chunks", () => {
|
|
62
|
+
it("saves and retrieves chunks", () => {
|
|
63
|
+
saveStreamChunk(1, 1, 10, Buffer.from("chunk-1"));
|
|
64
|
+
saveStreamChunk(1, 11, 20, Buffer.from("chunk-2"));
|
|
65
|
+
saveStreamChunk(1, 21, 30, Buffer.from("chunk-3"));
|
|
66
|
+
|
|
67
|
+
const chunks = getChunksSince(1, 0);
|
|
68
|
+
expect(chunks).toHaveLength(3);
|
|
69
|
+
expect(chunks[0].seqFrom).toBe(1);
|
|
70
|
+
expect(chunks[0].data.toString()).toBe("chunk-1");
|
|
71
|
+
expect(chunks[2].seqFrom).toBe(21);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("filters chunks by seq (returns chunks whose seq_to > sinceSeq)", () => {
|
|
75
|
+
// sinceSeq=15: chunk-2 (seq_to=20 > 15) and chunk-3 (seq_to=30 > 15) returned
|
|
76
|
+
const chunks = getChunksSince(1, 15);
|
|
77
|
+
expect(chunks).toHaveLength(2);
|
|
78
|
+
expect(chunks[0].seqFrom).toBe(11);
|
|
79
|
+
expect(chunks[1].seqFrom).toBe(21);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("returns empty for future seq", () => {
|
|
83
|
+
const chunks = getChunksSince(1, 100);
|
|
84
|
+
expect(chunks).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns empty for non-existent tab", () => {
|
|
88
|
+
const chunks = getChunksSince(999, 0);
|
|
89
|
+
expect(chunks).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("tab_snapshots", () => {
|
|
94
|
+
it("saves and retrieves snapshot", () => {
|
|
95
|
+
saveTabSnapshot(1, 25, 80, 24, Buffer.from("snapshot-data"));
|
|
96
|
+
|
|
97
|
+
const snapshot = getLatestSnapshot(1);
|
|
98
|
+
expect(snapshot).not.toBeNull();
|
|
99
|
+
expect(snapshot.seq).toBe(25);
|
|
100
|
+
expect(snapshot.cols).toBe(80);
|
|
101
|
+
expect(snapshot.rows).toBe(24);
|
|
102
|
+
expect(snapshot.snapshot.toString()).toBe("snapshot-data");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns latest snapshot when multiple exist", () => {
|
|
106
|
+
saveTabSnapshot(1, 50, 100, 30, Buffer.from("newer-snapshot"));
|
|
107
|
+
|
|
108
|
+
const snapshot = getLatestSnapshot(1);
|
|
109
|
+
expect(snapshot.seq).toBe(50);
|
|
110
|
+
expect(snapshot.snapshot.toString()).toBe("newer-snapshot");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns null for non-existent tab", () => {
|
|
114
|
+
const snapshot = getLatestSnapshot(999);
|
|
115
|
+
expect(snapshot).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("device_tab_cursors", () => {
|
|
120
|
+
it("creates cursor on first update", () => {
|
|
121
|
+
updateDeviceCursor("device-a", 1, 15);
|
|
122
|
+
|
|
123
|
+
const cursor = getDeviceCursor("device-a", 1);
|
|
124
|
+
expect(cursor).not.toBeNull();
|
|
125
|
+
expect(cursor.lastAckedSeq).toBe(15);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("updates existing cursor", () => {
|
|
129
|
+
updateDeviceCursor("device-a", 1, 30);
|
|
130
|
+
|
|
131
|
+
const cursor = getDeviceCursor("device-a", 1);
|
|
132
|
+
expect(cursor.lastAckedSeq).toBe(30);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("tracks per device and per tab", () => {
|
|
136
|
+
updateDeviceCursor("device-a", 2, 5);
|
|
137
|
+
updateDeviceCursor("device-b", 1, 10);
|
|
138
|
+
|
|
139
|
+
const cursorA1 = getDeviceCursor("device-a", 1);
|
|
140
|
+
const cursorA2 = getDeviceCursor("device-a", 2);
|
|
141
|
+
const cursorB1 = getDeviceCursor("device-b", 1);
|
|
142
|
+
|
|
143
|
+
expect(cursorA1.lastAckedSeq).toBe(30);
|
|
144
|
+
expect(cursorA2.lastAckedSeq).toBe(5);
|
|
145
|
+
expect(cursorB1.lastAckedSeq).toBe(10);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns null for non-existent cursor", () => {
|
|
149
|
+
const cursor = getDeviceCursor("device-x", 999);
|
|
150
|
+
expect(cursor).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|