@neurocode-ai/tui 1.18.8
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/bunfig.toml +4 -0
- package/package.json +72 -0
- package/src/app.tsx +1134 -0
- package/src/attention.ts +260 -0
- package/src/audio.d.ts +9 -0
- package/src/audio.ts +53 -0
- package/src/clipboard.ts +124 -0
- package/src/component/bg-pulse-render.ts +436 -0
- package/src/component/bg-pulse.tsx +99 -0
- package/src/component/command-palette.tsx +79 -0
- package/src/component/dialog-agent.tsx +31 -0
- package/src/component/dialog-console-org.tsx +135 -0
- package/src/component/dialog-debug.tsx +90 -0
- package/src/component/dialog-mcp.tsx +85 -0
- package/src/component/dialog-model.tsx +197 -0
- package/src/component/dialog-move-session.tsx +353 -0
- package/src/component/dialog-provider.tsx +469 -0
- package/src/component/dialog-retry-action.tsx +160 -0
- package/src/component/dialog-session-delete-failed.tsx +99 -0
- package/src/component/dialog-session-list.tsx +364 -0
- package/src/component/dialog-session-rename.tsx +31 -0
- package/src/component/dialog-skill.tsx +70 -0
- package/src/component/dialog-stash.tsx +87 -0
- package/src/component/dialog-status.tsx +168 -0
- package/src/component/dialog-tag.tsx +47 -0
- package/src/component/dialog-theme-list.tsx +50 -0
- package/src/component/dialog-variant.tsx +39 -0
- package/src/component/dialog-workspace-create.tsx +308 -0
- package/src/component/dialog-workspace-file-changes.tsx +144 -0
- package/src/component/dialog-workspace-list.tsx +112 -0
- package/src/component/dialog-workspace-unavailable.tsx +69 -0
- package/src/component/error-component.tsx +240 -0
- package/src/component/logo.tsx +61 -0
- package/src/component/plugin-route-missing.tsx +14 -0
- package/src/component/prompt/autocomplete.tsx +781 -0
- package/src/component/prompt/cwd.ts +0 -0
- package/src/component/prompt/frecency.tsx +1 -0
- package/src/component/prompt/history.tsx +1 -0
- package/src/component/prompt/index.tsx +1713 -0
- package/src/component/prompt/local-attachment.ts +48 -0
- package/src/component/prompt/move.tsx +205 -0
- package/src/component/prompt/stash.tsx +1 -0
- package/src/component/prompt/workspace.tsx +137 -0
- package/src/component/register-spinner.ts +6 -0
- package/src/component/spinner.tsx +26 -0
- package/src/component/startup-loading.tsx +63 -0
- package/src/component/todo-item.tsx +32 -0
- package/src/component/use-connected.tsx +12 -0
- package/src/component/workspace-label.tsx +19 -0
- package/src/config/index.tsx +129 -0
- package/src/config/keybind.ts +471 -0
- package/src/context/args.tsx +16 -0
- package/src/context/clipboard.tsx +18 -0
- package/src/context/data.tsx +569 -0
- package/src/context/directory.ts +17 -0
- package/src/context/editor.ts +408 -0
- package/src/context/epilogue.tsx +6 -0
- package/src/context/event.ts +36 -0
- package/src/context/exit.tsx +8 -0
- package/src/context/helper.tsx +26 -0
- package/src/context/kv.tsx +66 -0
- package/src/context/local.tsx +542 -0
- package/src/context/location.tsx +14 -0
- package/src/context/path-format.tsx +24 -0
- package/src/context/permission.tsx +26 -0
- package/src/context/project.tsx +115 -0
- package/src/context/prompt.tsx +18 -0
- package/src/context/route.tsx +60 -0
- package/src/context/runtime.tsx +62 -0
- package/src/context/sdk.tsx +151 -0
- package/src/context/sync.tsx +666 -0
- package/src/context/theme.tsx +332 -0
- package/src/context/thinking.ts +67 -0
- package/src/editor-zed.ts +286 -0
- package/src/editor.ts +101 -0
- package/src/feature-plugins/builtins.ts +36 -0
- package/src/feature-plugins/home/footer.tsx +100 -0
- package/src/feature-plugins/home/tips-view.tsx +287 -0
- package/src/feature-plugins/home/tips.tsx +59 -0
- package/src/feature-plugins/sidebar/context.tsx +65 -0
- package/src/feature-plugins/sidebar/files.tsx +70 -0
- package/src/feature-plugins/sidebar/footer.tsx +98 -0
- package/src/feature-plugins/sidebar/lsp.tsx +65 -0
- package/src/feature-plugins/sidebar/mcp.tsx +97 -0
- package/src/feature-plugins/sidebar/todo.tsx +49 -0
- package/src/feature-plugins/system/diff-viewer-file-tree-utils.ts +232 -0
- package/src/feature-plugins/system/diff-viewer-file-tree.tsx +162 -0
- package/src/feature-plugins/system/diff-viewer-ui.tsx +103 -0
- package/src/feature-plugins/system/diff-viewer.tsx +1077 -0
- package/src/feature-plugins/system/notifications.ts +94 -0
- package/src/feature-plugins/system/plugins.tsx +269 -0
- package/src/feature-plugins/system/which-key.tsx +608 -0
- package/src/index.tsx +1 -0
- package/src/keymap.tsx +290 -0
- package/src/logo.ts +11 -0
- package/src/parsers-config.ts +386 -0
- package/src/plugin/adapters.tsx +355 -0
- package/src/plugin/api.ts +52 -0
- package/src/plugin/command-shim.ts +109 -0
- package/src/plugin/runtime.tsx +81 -0
- package/src/plugin/slots.tsx +65 -0
- package/src/prompt/display.ts +48 -0
- package/src/prompt/frecency.tsx +80 -0
- package/src/prompt/history.tsx +111 -0
- package/src/prompt/part.ts +29 -0
- package/src/prompt/stash.tsx +89 -0
- package/src/prompt/traits.ts +29 -0
- package/src/routes/home/session-destination.tsx +41 -0
- package/src/routes/home.tsx +95 -0
- package/src/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/routes/session/dialog-message.tsx +109 -0
- package/src/routes/session/dialog-subagent.tsx +26 -0
- package/src/routes/session/dialog-timeline.tsx +47 -0
- package/src/routes/session/footer.tsx +91 -0
- package/src/routes/session/index.tsx +2710 -0
- package/src/routes/session/permission.tsx +718 -0
- package/src/routes/session/question.tsx +514 -0
- package/src/routes/session/sidebar.tsx +103 -0
- package/src/routes/session/subagent-footer.tsx +132 -0
- package/src/runtime.tsx +9 -0
- package/src/terminal-win32.ts +130 -0
- package/src/theme/assets/aura.json +69 -0
- package/src/theme/assets/ayu.json +80 -0
- package/src/theme/assets/carbonfox.json +248 -0
- package/src/theme/assets/catppuccin-frappe.json +230 -0
- package/src/theme/assets/catppuccin-macchiato.json +230 -0
- package/src/theme/assets/catppuccin.json +112 -0
- package/src/theme/assets/cobalt2.json +225 -0
- package/src/theme/assets/cursor.json +249 -0
- package/src/theme/assets/dracula.json +219 -0
- package/src/theme/assets/everforest.json +241 -0
- package/src/theme/assets/flexoki.json +237 -0
- package/src/theme/assets/github.json +233 -0
- package/src/theme/assets/gruvbox.json +242 -0
- package/src/theme/assets/kanagawa.json +77 -0
- package/src/theme/assets/lucent-orng.json +234 -0
- package/src/theme/assets/material.json +235 -0
- package/src/theme/assets/matrix.json +77 -0
- package/src/theme/assets/mercury.json +252 -0
- package/src/theme/assets/monokai.json +221 -0
- package/src/theme/assets/nightowl.json +221 -0
- package/src/theme/assets/nord.json +223 -0
- package/src/theme/assets/one-dark.json +84 -0
- package/src/theme/assets/opencode.json +245 -0
- package/src/theme/assets/orng.json +249 -0
- package/src/theme/assets/osaka-jade.json +93 -0
- package/src/theme/assets/palenight.json +222 -0
- package/src/theme/assets/rosepine.json +234 -0
- package/src/theme/assets/solarized.json +223 -0
- package/src/theme/assets/synthwave84.json +226 -0
- package/src/theme/assets/tokyonight.json +243 -0
- package/src/theme/assets/vercel.json +245 -0
- package/src/theme/assets/vesper.json +218 -0
- package/src/theme/assets/zenburn.json +223 -0
- package/src/theme/index.ts +1089 -0
- package/src/ui/border.ts +21 -0
- package/src/ui/dialog-alert.tsx +66 -0
- package/src/ui/dialog-confirm.tsx +108 -0
- package/src/ui/dialog-export-options.tsx +217 -0
- package/src/ui/dialog-help.tsx +40 -0
- package/src/ui/dialog-prompt.tsx +126 -0
- package/src/ui/dialog-select.tsx +790 -0
- package/src/ui/dialog.tsx +231 -0
- package/src/ui/link.tsx +34 -0
- package/src/ui/spinner.ts +368 -0
- package/src/ui/toast.tsx +102 -0
- package/src/util/collapse-tool-output.ts +19 -0
- package/src/util/error.ts +182 -0
- package/src/util/filetype.ts +130 -0
- package/src/util/format.ts +20 -0
- package/src/util/layout.ts +25 -0
- package/src/util/locale.ts +86 -0
- package/src/util/model.ts +28 -0
- package/src/util/path.ts +12 -0
- package/src/util/persistence.ts +33 -0
- package/src/util/presentation.ts +38 -0
- package/src/util/provider-origin.ts +7 -0
- package/src/util/record.ts +3 -0
- package/src/util/renderer.ts +7 -0
- package/src/util/revert-diff.ts +18 -0
- package/src/util/scroll.ts +27 -0
- package/src/util/selection.ts +79 -0
- package/src/util/session.ts +3 -0
- package/src/util/signal.ts +51 -0
- package/src/util/system.ts +20 -0
- package/src/util/tool-display.ts +13 -0
- package/src/util/transcript.ts +112 -0
- package/sst-env.d.ts +10 -0
- package/test/app-lifecycle.test.tsx +128 -0
- package/test/cli/cmd/tui/dialog-workspace-create.test.ts +28 -0
- package/test/cli/cmd/tui/model-options.test.ts +32 -0
- package/test/cli/cmd/tui/notifications.test.ts +267 -0
- package/test/cli/cmd/tui/provider-options.test.ts +41 -0
- package/test/cli/cmd/tui/sync-fixture.tsx +70 -0
- package/test/cli/cmd/tui/sync-live-hydration.test.tsx +262 -0
- package/test/cli/cmd/tui/sync-undefined-messages.test.tsx +43 -0
- package/test/cli/cmd/tui/sync.test.tsx +65 -0
- package/test/cli/tui/__snapshots__/inline-tool-wrap-snapshot.test.tsx.snap +92 -0
- package/test/cli/tui/data.test.tsx +486 -0
- package/test/cli/tui/dialog-prompt.test.tsx +147 -0
- package/test/cli/tui/diff-viewer-file-tree.test.tsx +200 -0
- package/test/cli/tui/diff-viewer.test.tsx +268 -0
- package/test/cli/tui/inline-tool-wrap-snapshot.test.tsx +351 -0
- package/test/cli/tui/prompt-submit-race.test.ts +98 -0
- package/test/cli/tui/thinking.test.ts +36 -0
- package/test/cli/tui/use-event.test.tsx +148 -0
- package/test/clipboard.test.ts +19 -0
- package/test/component/dialog-session-list.test.ts +46 -0
- package/test/config.test.tsx +127 -0
- package/test/context/local.test.ts +22 -0
- package/test/editor.test.ts +32 -0
- package/test/feature-plugins/diff-viewer-file-tree-utils.test.ts +323 -0
- package/test/fixture/fixture.ts +13 -0
- package/test/fixture/tui-environment.tsx +32 -0
- package/test/fixture/tui-plugin.ts +36 -0
- package/test/fixture/tui-runtime.ts +12 -0
- package/test/fixture/tui-sdk.ts +109 -0
- package/test/index.test.tsx +6 -0
- package/test/keymap.test.tsx +141 -0
- package/test/plugin/runtime.test.ts +50 -0
- package/test/plugin/slots.test.tsx +38 -0
- package/test/prompt/display.test.ts +33 -0
- package/test/prompt/history.test.ts +39 -0
- package/test/prompt/jsonl.test.ts +24 -0
- package/test/prompt/local-attachment.test.ts +43 -0
- package/test/prompt/part.test.ts +53 -0
- package/test/prompt/persistence.test.ts +23 -0
- package/test/prompt/traits.test.ts +25 -0
- package/test/runtime.test.tsx +37 -0
- package/test/theme.test.ts +81 -0
- package/test/util/error.test.ts +49 -0
- package/test/util/filetype.test.ts +16 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/model.test.ts +9 -0
- package/test/util/presentation.test.ts +8 -0
- package/test/util/renderer.test.ts +30 -0
- package/test/util/revert-diff.test.ts +35 -0
- package/test/util/session.test.ts +10 -0
- package/test/util/tool-display.test.ts +40 -0
- package/test/util/transcript.test.ts +421 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { describe, expect, test } from "bun:test"
|
|
3
|
+
import { tmpdir } from "../../../fixture/fixture"
|
|
4
|
+
import { mount, wait } from "./sync-fixture"
|
|
5
|
+
import type { GlobalEvent } from "@neurocode-ai/sdk/v2"
|
|
6
|
+
|
|
7
|
+
function branchEvent(branch: string, workspace?: string): GlobalEvent {
|
|
8
|
+
return {
|
|
9
|
+
directory: "/tmp/other",
|
|
10
|
+
project: "proj_test",
|
|
11
|
+
workspace,
|
|
12
|
+
payload: {
|
|
13
|
+
id: `evt_vcs_${branch}`,
|
|
14
|
+
type: "vcs.branch.updated",
|
|
15
|
+
properties: { branch },
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("tui sync", () => {
|
|
21
|
+
test("refresh scopes sessions by default and lists project sessions when disabled", async () => {
|
|
22
|
+
await using tmp = await tmpdir()
|
|
23
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
24
|
+
const { app, kv, sync, session } = await mount(undefined, tmp.path)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
expect(kv.get("session_directory_filter_enabled", true)).toBe(true)
|
|
28
|
+
expect(session.at(-1)?.searchParams.get("roots")).toBeNull()
|
|
29
|
+
expect(session.at(-1)?.searchParams.get("scope")).toBeNull()
|
|
30
|
+
expect(session.at(-1)?.searchParams.get("path")).toBe("packages/tui")
|
|
31
|
+
|
|
32
|
+
kv.set("session_directory_filter_enabled", false)
|
|
33
|
+
await sync.session.refresh()
|
|
34
|
+
|
|
35
|
+
expect(session.at(-1)?.searchParams.get("scope")).toBe("project")
|
|
36
|
+
expect(session.at(-1)?.searchParams.get("path")).toBeNull()
|
|
37
|
+
expect(session.at(-1)?.searchParams.get("roots")).toBeNull()
|
|
38
|
+
} finally {
|
|
39
|
+
app.renderer.destroy()
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("vcs branch updates only apply for the active workspace", async () => {
|
|
44
|
+
await using tmp = await tmpdir()
|
|
45
|
+
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
|
46
|
+
const { app, emit, project, sync } = await mount(undefined, tmp.path)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
expect(sync.data.vcs?.branch).toBe("main")
|
|
50
|
+
|
|
51
|
+
project.workspace.set("ws_a")
|
|
52
|
+
emit(branchEvent("other", "ws_b"))
|
|
53
|
+
await Bun.sleep(30)
|
|
54
|
+
|
|
55
|
+
expect(sync.data.vcs?.branch).toBe("main")
|
|
56
|
+
|
|
57
|
+
emit(branchEvent("feature", "ws_a"))
|
|
58
|
+
await wait(() => sync.data.vcs?.branch === "feature")
|
|
59
|
+
|
|
60
|
+
expect(sync.data.vcs?.branch).toBe("feature")
|
|
61
|
+
} finally {
|
|
62
|
+
app.renderer.destroy()
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
|
2
|
+
|
|
3
|
+
exports[`TUI inline tool wrapping snapshots consecutive grep, glob, and read rows at a narrow width 1`] = `
|
|
4
|
+
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
|
|
5
|
+
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
|
|
6
|
+
|
|
7
|
+
✱ Glob "**/*db*" in packages/opencode (6 matches)
|
|
8
|
+
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
|
|
9
|
+
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
|
|
10
|
+
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
|
11
|
+
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
exports[`TUI inline tool wrapping snapshots expanded tool errors under the tool text 1`] = `
|
|
15
|
+
" ✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
|
|
16
|
+
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
|
|
17
|
+
|
|
18
|
+
✱ Glob "**/*db*" in packages/opencode (6 matches)
|
|
19
|
+
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
|
|
20
|
+
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
|
|
21
|
+
No LSP server available for this file type.
|
|
22
|
+
|
|
23
|
+
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
|
24
|
+
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`TUI inline tool wrapping keeps separation after a shell output block 1`] = `
|
|
28
|
+
"
|
|
29
|
+
|
|
30
|
+
$ ls
|
|
31
|
+
|
|
32
|
+
file.ts
|
|
33
|
+
|
|
34
|
+
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
|
|
35
|
+
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
|
|
36
|
+
|
|
37
|
+
✱ Glob "**/*db*" in packages/opencode (6 matches)
|
|
38
|
+
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
|
|
39
|
+
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
|
|
40
|
+
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
|
41
|
+
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
exports[`TUI inline tool wrapping keeps separation after a padded user message 1`] = `
|
|
45
|
+
"
|
|
46
|
+
Check whether the next tool remains separated.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
✱ Grep "OPENCODE.*DB|database|sqlite|drizzle|dev.*db|data.
|
|
50
|
+
*dir|xdg|APPDATA" in packages/opencode/src (151 matches)
|
|
51
|
+
|
|
52
|
+
✱ Glob "**/*db*" in packages/opencode (6 matches)
|
|
53
|
+
→ Read packages/opencode/src/storage/db.ts [offset=1, limit=130]
|
|
54
|
+
→ Read packages/opencode/src/index.ts [offset=1, limit=100]
|
|
55
|
+
✱ Grep "export const OPENCODE_DB|OPENCODE_DB|OPENCODE_DEV|Global\\.
|
|
56
|
+
Path\\.data|data =" in packages/opencode/src (115 matches)"
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
exports[`TUI inline tool wrapping separates after a multi-line task row 1`] = `
|
|
60
|
+
" ✱ Grep "Task" (2 matches)
|
|
61
|
+
|
|
62
|
+
⠙ Explore Task — Inspect active task spacing
|
|
63
|
+
|
|
64
|
+
✓ General Task — Confirm completed task spacing
|
|
65
|
+
↳ 1 toolcall · 501ms
|
|
66
|
+
|
|
67
|
+
→ Read src/cli/cmd/tui/routes/session/index.tsx"
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
exports[`TUI inline tool wrapping separates a task row from a preceding inline detail 1`] = `
|
|
71
|
+
" → Read src/cli/cmd/tui/routes/session/index.tsx
|
|
72
|
+
↳ Loaded src/cli/cmd/tui/routes/session/tools.tsx
|
|
73
|
+
|
|
74
|
+
✓ Explore Task — Inspect active task spacing
|
|
75
|
+
↳ 1 toolcall · 501ms"
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
exports[`TUI inline tool wrapping separates an inline row from the previous assistant summary 1`] = `
|
|
79
|
+
" ▣ Build · Little Frank · 53.1s
|
|
80
|
+
|
|
81
|
+
✓ Build Task — Review changes
|
|
82
|
+
↳ 48 toolcalls · 1m 40s"
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
exports[`TUI inline tool wrapping separates an inline row from the previous assistant error 1`] = `
|
|
86
|
+
"│
|
|
87
|
+
│ Managed inference requires an active Member plan
|
|
88
|
+
│
|
|
89
|
+
|
|
90
|
+
✓ Build Task — Review changes
|
|
91
|
+
↳ 48 toolcalls · 1m 40s"
|
|
92
|
+
`;
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import { expect, test } from "bun:test"
|
|
3
|
+
import { testRender } from "@opentui/solid"
|
|
4
|
+
import type { Event, GlobalEvent } from "@neurocode-ai/sdk/v2"
|
|
5
|
+
import { onMount } from "solid-js"
|
|
6
|
+
import { ProjectProvider } from "../../../src/context/project"
|
|
7
|
+
import { SDKProvider } from "../../../src/context/sdk"
|
|
8
|
+
import { DataProvider, useData } from "../../../src/context/data"
|
|
9
|
+
import { createEventSource, createFetch, directory, json } from "../../fixture/tui-sdk"
|
|
10
|
+
import { TestTuiContexts } from "../../fixture/tui-environment"
|
|
11
|
+
|
|
12
|
+
async function wait(fn: () => boolean, timeout = 2000) {
|
|
13
|
+
const start = Date.now()
|
|
14
|
+
while (!fn()) {
|
|
15
|
+
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
|
|
16
|
+
await Bun.sleep(10)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function global(payload: Event): GlobalEvent {
|
|
21
|
+
return { directory, project: "proj_test", payload }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emitEvent(events: ReturnType<typeof createEventSource>, payload: Event) {
|
|
25
|
+
events.emit(global(payload))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("refreshes resources into reactive getters", async () => {
|
|
29
|
+
const events = createEventSource()
|
|
30
|
+
const location = {
|
|
31
|
+
directory,
|
|
32
|
+
project: { id: "proj_test", directory },
|
|
33
|
+
}
|
|
34
|
+
const calls = createFetch((url) => {
|
|
35
|
+
if (url.pathname === "/api/session/ses_test")
|
|
36
|
+
return json({
|
|
37
|
+
data: {
|
|
38
|
+
id: "ses_test",
|
|
39
|
+
projectID: "proj_test",
|
|
40
|
+
cost: 0,
|
|
41
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
42
|
+
time: { created: 0, updated: 0 },
|
|
43
|
+
title: "Test session",
|
|
44
|
+
location: { directory },
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
if (url.pathname === "/api/agent")
|
|
48
|
+
return json({
|
|
49
|
+
location,
|
|
50
|
+
data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }],
|
|
51
|
+
})
|
|
52
|
+
return undefined
|
|
53
|
+
}, events)
|
|
54
|
+
let data!: ReturnType<typeof useData>
|
|
55
|
+
let ready!: () => void
|
|
56
|
+
const mounted = new Promise<void>((resolve) => {
|
|
57
|
+
ready = resolve
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
function Probe() {
|
|
61
|
+
data = useData()
|
|
62
|
+
onMount(ready)
|
|
63
|
+
return <box />
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const app = await testRender(() => (
|
|
67
|
+
<TestTuiContexts>
|
|
68
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
69
|
+
<ProjectProvider>
|
|
70
|
+
<DataProvider>
|
|
71
|
+
<Probe />
|
|
72
|
+
</DataProvider>
|
|
73
|
+
</ProjectProvider>
|
|
74
|
+
</SDKProvider>
|
|
75
|
+
</TestTuiContexts>
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await mounted
|
|
80
|
+
expect(data.location.default()).toEqual({ directory })
|
|
81
|
+
expect(data.session.get("ses_test")).toBeUndefined()
|
|
82
|
+
expect(data.location.agent.list(location)).toBeUndefined()
|
|
83
|
+
|
|
84
|
+
await data.session.refresh("ses_test")
|
|
85
|
+
await data.location.agent.refresh()
|
|
86
|
+
|
|
87
|
+
expect(data.session.get("ses_test")?.title).toBe("Test session")
|
|
88
|
+
expect(data.location.default()).toEqual({ directory, workspaceID: undefined })
|
|
89
|
+
expect(data.location.agent.list(location)?.map((agent) => agent.id)).toEqual(["build"])
|
|
90
|
+
} finally {
|
|
91
|
+
app.renderer.destroy()
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("refreshes integrations after integration updates", async () => {
|
|
96
|
+
const events = createEventSource()
|
|
97
|
+
const requests = { integration: 0, model: 0, provider: 0 }
|
|
98
|
+
const calls = createFetch((url) => {
|
|
99
|
+
if (url.pathname === "/api/model") {
|
|
100
|
+
requests.model++
|
|
101
|
+
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
|
|
102
|
+
}
|
|
103
|
+
if (url.pathname === "/api/provider") {
|
|
104
|
+
requests.provider++
|
|
105
|
+
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
|
|
106
|
+
}
|
|
107
|
+
if (url.pathname !== "/api/integration") return
|
|
108
|
+
requests.integration++
|
|
109
|
+
return json({
|
|
110
|
+
location: { directory, project: { id: "proj_test", directory } },
|
|
111
|
+
data:
|
|
112
|
+
requests.integration === 1
|
|
113
|
+
? []
|
|
114
|
+
: [
|
|
115
|
+
{
|
|
116
|
+
id: "openai",
|
|
117
|
+
name: "OpenAI",
|
|
118
|
+
methods: [{ type: "key" }],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
})
|
|
122
|
+
}, events)
|
|
123
|
+
let data!: ReturnType<typeof useData>
|
|
124
|
+
let ready!: () => void
|
|
125
|
+
const mounted = new Promise<void>((resolve) => {
|
|
126
|
+
ready = resolve
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
function Probe() {
|
|
130
|
+
data = useData()
|
|
131
|
+
onMount(ready)
|
|
132
|
+
return <box />
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const app = await testRender(() => (
|
|
136
|
+
<TestTuiContexts>
|
|
137
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
138
|
+
<ProjectProvider>
|
|
139
|
+
<DataProvider>
|
|
140
|
+
<Probe />
|
|
141
|
+
</DataProvider>
|
|
142
|
+
</ProjectProvider>
|
|
143
|
+
</SDKProvider>
|
|
144
|
+
</TestTuiContexts>
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await mounted
|
|
149
|
+
await wait(() => data.location.integration.list() !== undefined)
|
|
150
|
+
expect(data.location.integration.list()).toEqual([])
|
|
151
|
+
const before = { ...requests }
|
|
152
|
+
|
|
153
|
+
emitEvent(events, { id: "evt_integration", type: "integration.updated", properties: {} })
|
|
154
|
+
await wait(() => data.location.integration.list()?.length === 1)
|
|
155
|
+
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
|
156
|
+
expect(data.location.integration.list()?.[0]).toMatchObject({ id: "openai", name: "OpenAI" })
|
|
157
|
+
} finally {
|
|
158
|
+
app.renderer.destroy()
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("refreshes effective catalog data after catalog updates", async () => {
|
|
163
|
+
const events = createEventSource()
|
|
164
|
+
const requests = { model: 0, provider: 0 }
|
|
165
|
+
const calls = createFetch((url) => {
|
|
166
|
+
if (url.pathname === "/api/model") {
|
|
167
|
+
requests.model++
|
|
168
|
+
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
|
|
169
|
+
}
|
|
170
|
+
if (url.pathname === "/api/provider") {
|
|
171
|
+
requests.provider++
|
|
172
|
+
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
|
|
173
|
+
}
|
|
174
|
+
}, events)
|
|
175
|
+
|
|
176
|
+
const app = await testRender(() => (
|
|
177
|
+
<TestTuiContexts>
|
|
178
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
179
|
+
<ProjectProvider>
|
|
180
|
+
<DataProvider>
|
|
181
|
+
<box />
|
|
182
|
+
</DataProvider>
|
|
183
|
+
</ProjectProvider>
|
|
184
|
+
</SDKProvider>
|
|
185
|
+
</TestTuiContexts>
|
|
186
|
+
))
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await wait(() => requests.model > 0 && requests.provider > 0)
|
|
190
|
+
const before = { ...requests }
|
|
191
|
+
emitEvent(events, { id: "evt_catalog", type: "catalog.updated", properties: {} })
|
|
192
|
+
await wait(() => requests.model > before.model && requests.provider > before.provider)
|
|
193
|
+
} finally {
|
|
194
|
+
app.renderer.destroy()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test("refreshes references after updates", async () => {
|
|
199
|
+
const events = createEventSource()
|
|
200
|
+
let requests = 0
|
|
201
|
+
const calls = createFetch((url) => {
|
|
202
|
+
if (url.pathname !== "/api/reference") return
|
|
203
|
+
requests++
|
|
204
|
+
return json({
|
|
205
|
+
location: { directory, project: { id: "proj_test", directory } },
|
|
206
|
+
data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }],
|
|
207
|
+
})
|
|
208
|
+
}, events)
|
|
209
|
+
let data!: ReturnType<typeof useData>
|
|
210
|
+
let ready!: () => void
|
|
211
|
+
const mounted = new Promise<void>((resolve) => {
|
|
212
|
+
ready = resolve
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
function Probe() {
|
|
216
|
+
data = useData()
|
|
217
|
+
onMount(ready)
|
|
218
|
+
return <box />
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const app = await testRender(() => (
|
|
222
|
+
<TestTuiContexts>
|
|
223
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
224
|
+
<ProjectProvider>
|
|
225
|
+
<DataProvider>
|
|
226
|
+
<Probe />
|
|
227
|
+
</DataProvider>
|
|
228
|
+
</ProjectProvider>
|
|
229
|
+
</SDKProvider>
|
|
230
|
+
</TestTuiContexts>
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
await mounted
|
|
235
|
+
await wait(() => requests === 1)
|
|
236
|
+
emitEvent(events, { id: "evt_reference_1", type: "reference.updated", properties: {} })
|
|
237
|
+
await wait(() => data.location.reference.list()?.length === 1)
|
|
238
|
+
expect(data.location.reference.list()?.[0]?.name).toBe("docs")
|
|
239
|
+
} finally {
|
|
240
|
+
app.renderer.destroy()
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("settles pending tools when a live failure arrives", async () => {
|
|
245
|
+
const events = createEventSource()
|
|
246
|
+
const calls = createFetch(undefined, events)
|
|
247
|
+
let sync!: ReturnType<typeof useData>
|
|
248
|
+
let ready!: () => void
|
|
249
|
+
const mounted = new Promise<void>((resolve) => {
|
|
250
|
+
ready = resolve
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
function Probe() {
|
|
254
|
+
sync = useData()
|
|
255
|
+
onMount(ready)
|
|
256
|
+
return <box />
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const app = await testRender(() => (
|
|
260
|
+
<TestTuiContexts>
|
|
261
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
262
|
+
<ProjectProvider>
|
|
263
|
+
<DataProvider>
|
|
264
|
+
<Probe />
|
|
265
|
+
</DataProvider>
|
|
266
|
+
</ProjectProvider>
|
|
267
|
+
</SDKProvider>
|
|
268
|
+
</TestTuiContexts>
|
|
269
|
+
))
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await mounted
|
|
273
|
+
emitEvent(events, {
|
|
274
|
+
id: "evt_agent_1",
|
|
275
|
+
type: "session.next.agent.switched",
|
|
276
|
+
properties: { sessionID: "session-1", messageID: "msg_agent_1", timestamp: 0, agent: "build" },
|
|
277
|
+
})
|
|
278
|
+
emitEvent(events, {
|
|
279
|
+
id: "evt_model_1",
|
|
280
|
+
type: "session.next.model.switched",
|
|
281
|
+
properties: {
|
|
282
|
+
sessionID: "session-1",
|
|
283
|
+
messageID: "msg_model_1",
|
|
284
|
+
timestamp: 0,
|
|
285
|
+
model: { id: "model-1", providerID: "provider-1" },
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
emitEvent(events, {
|
|
289
|
+
id: "evt_step_started_1",
|
|
290
|
+
type: "session.next.step.started",
|
|
291
|
+
properties: {
|
|
292
|
+
sessionID: "session-1",
|
|
293
|
+
assistantMessageID: "msg_explicit_assistant_9",
|
|
294
|
+
timestamp: 1,
|
|
295
|
+
agent: "build",
|
|
296
|
+
model: { id: "model-1", providerID: "provider-1" },
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
emitEvent(events, {
|
|
300
|
+
id: "evt_input_1",
|
|
301
|
+
type: "session.next.tool.input.started",
|
|
302
|
+
properties: {
|
|
303
|
+
sessionID: "session-1",
|
|
304
|
+
assistantMessageID: "msg_explicit_assistant_9",
|
|
305
|
+
timestamp: 2,
|
|
306
|
+
callID: "call-1",
|
|
307
|
+
name: "bash",
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
emitEvent(events, {
|
|
311
|
+
id: "evt_called_1",
|
|
312
|
+
type: "session.next.tool.called",
|
|
313
|
+
properties: {
|
|
314
|
+
sessionID: "session-1",
|
|
315
|
+
timestamp: 2,
|
|
316
|
+
assistantMessageID: "msg_explicit_assistant_9",
|
|
317
|
+
callID: "call-1",
|
|
318
|
+
tool: "bash",
|
|
319
|
+
input: {},
|
|
320
|
+
provider: { executed: false, metadata: { fake: { call: true } } },
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
emitEvent(events, {
|
|
324
|
+
id: "evt_failed_1",
|
|
325
|
+
type: "session.next.tool.failed",
|
|
326
|
+
properties: {
|
|
327
|
+
sessionID: "session-1",
|
|
328
|
+
timestamp: 3,
|
|
329
|
+
assistantMessageID: "msg_explicit_assistant_9",
|
|
330
|
+
callID: "call-1",
|
|
331
|
+
error: { type: "unknown", message: "aborted" },
|
|
332
|
+
provider: { executed: false, metadata: { fake: { result: true } } },
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
await wait(() => {
|
|
337
|
+
const assistant = sync.session.message.list("session-1")?.[0]
|
|
338
|
+
return (
|
|
339
|
+
assistant?.type === "assistant" &&
|
|
340
|
+
assistant.content[0]?.type === "tool" &&
|
|
341
|
+
assistant.content[0].state.status === "error"
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const assistant = sync.session.message.list("session-1")?.[0]
|
|
346
|
+
expect(assistant?.type).toBe("assistant")
|
|
347
|
+
if (assistant?.type !== "assistant") return
|
|
348
|
+
expect(assistant.id).toBe("msg_explicit_assistant_9")
|
|
349
|
+
const tool = assistant.content[0]
|
|
350
|
+
expect(tool?.type).toBe("tool")
|
|
351
|
+
if (tool?.type !== "tool") return
|
|
352
|
+
expect(tool.state.status).toBe("error")
|
|
353
|
+
if (tool.state.status !== "error") return
|
|
354
|
+
expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
|
|
355
|
+
expect(tool.state.input).toEqual({})
|
|
356
|
+
expect(tool.state.structured).toEqual({})
|
|
357
|
+
expect(tool.state.content).toEqual([])
|
|
358
|
+
expect(tool.provider).toEqual({
|
|
359
|
+
executed: false,
|
|
360
|
+
metadata: { fake: { call: true } },
|
|
361
|
+
resultMetadata: { fake: { result: true } },
|
|
362
|
+
})
|
|
363
|
+
expect((sync.session.message.list("session-1") ?? []).map((message) => message.type)).toEqual([
|
|
364
|
+
"assistant",
|
|
365
|
+
"model-switched",
|
|
366
|
+
"agent-switched",
|
|
367
|
+
])
|
|
368
|
+
} finally {
|
|
369
|
+
app.renderer.destroy()
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test("renders admitted prompts only after they become model-visible", async () => {
|
|
374
|
+
const events = createEventSource()
|
|
375
|
+
const calls = createFetch(undefined, events)
|
|
376
|
+
let sync!: ReturnType<typeof useData>
|
|
377
|
+
let ready!: () => void
|
|
378
|
+
const mounted = new Promise<void>((resolve) => {
|
|
379
|
+
ready = resolve
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
function Probe() {
|
|
383
|
+
sync = useData()
|
|
384
|
+
onMount(ready)
|
|
385
|
+
return <box />
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const app = await testRender(() => (
|
|
389
|
+
<TestTuiContexts>
|
|
390
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
391
|
+
<ProjectProvider>
|
|
392
|
+
<DataProvider>
|
|
393
|
+
<Probe />
|
|
394
|
+
</DataProvider>
|
|
395
|
+
</ProjectProvider>
|
|
396
|
+
</SDKProvider>
|
|
397
|
+
</TestTuiContexts>
|
|
398
|
+
))
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
await mounted
|
|
402
|
+
emitEvent(events, {
|
|
403
|
+
id: "evt_admitted_1",
|
|
404
|
+
type: "session.next.prompt.admitted",
|
|
405
|
+
properties: {
|
|
406
|
+
sessionID: "session-1",
|
|
407
|
+
messageID: "msg_user_1",
|
|
408
|
+
timestamp: 0,
|
|
409
|
+
prompt: { text: "hello" },
|
|
410
|
+
delivery: "steer",
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
expect(sync.session.message.list("session-1") ?? []).toEqual([])
|
|
414
|
+
|
|
415
|
+
emitEvent(events, {
|
|
416
|
+
id: "evt_prompted_1",
|
|
417
|
+
type: "session.next.prompted",
|
|
418
|
+
properties: {
|
|
419
|
+
sessionID: "session-1",
|
|
420
|
+
messageID: "msg_user_1",
|
|
421
|
+
timestamp: 0,
|
|
422
|
+
prompt: { text: "hello" },
|
|
423
|
+
delivery: "steer",
|
|
424
|
+
},
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
|
428
|
+
const message = sync.session.message.list("session-1")?.[0]
|
|
429
|
+
expect(message?.type).toBe("user")
|
|
430
|
+
if (message?.type !== "user") return
|
|
431
|
+
expect(message).toMatchObject({ id: "msg_user_1", text: "hello" })
|
|
432
|
+
} finally {
|
|
433
|
+
app.renderer.destroy()
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
test("projects live context updates with their message ID", async () => {
|
|
438
|
+
const events = createEventSource()
|
|
439
|
+
const calls = createFetch(undefined, events)
|
|
440
|
+
let sync!: ReturnType<typeof useData>
|
|
441
|
+
let ready!: () => void
|
|
442
|
+
const mounted = new Promise<void>((resolve) => {
|
|
443
|
+
ready = resolve
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
function Probe() {
|
|
447
|
+
sync = useData()
|
|
448
|
+
onMount(ready)
|
|
449
|
+
return <box />
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const app = await testRender(() => (
|
|
453
|
+
<TestTuiContexts>
|
|
454
|
+
<SDKProvider url="http://test" directory={directory} events={events.source} fetch={calls.fetch}>
|
|
455
|
+
<ProjectProvider>
|
|
456
|
+
<DataProvider>
|
|
457
|
+
<Probe />
|
|
458
|
+
</DataProvider>
|
|
459
|
+
</ProjectProvider>
|
|
460
|
+
</SDKProvider>
|
|
461
|
+
</TestTuiContexts>
|
|
462
|
+
))
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
await mounted
|
|
466
|
+
emitEvent(events, {
|
|
467
|
+
id: "evt_context_1",
|
|
468
|
+
type: "session.next.context.updated",
|
|
469
|
+
properties: {
|
|
470
|
+
sessionID: "session-1",
|
|
471
|
+
messageID: "msg_context_1",
|
|
472
|
+
timestamp: 1,
|
|
473
|
+
text: "Updated context",
|
|
474
|
+
},
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
await wait(() => sync.session.message.list("session-1")?.length === 1)
|
|
478
|
+
expect(sync.session.message.list("session-1")?.[0]).toMatchObject({
|
|
479
|
+
id: "msg_context_1",
|
|
480
|
+
type: "system",
|
|
481
|
+
text: "Updated context",
|
|
482
|
+
})
|
|
483
|
+
} finally {
|
|
484
|
+
app.renderer.destroy()
|
|
485
|
+
}
|
|
486
|
+
})
|