@sigrun/tui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +3 -3
  3. package/dist/actions.d.ts +0 -78
  4. package/dist/actions.js +0 -12
  5. package/dist/api.d.ts +0 -147
  6. package/dist/api.js +0 -185
  7. package/dist/clipboard.d.ts +0 -20
  8. package/dist/clipboard.js +0 -19
  9. package/dist/editor-launch.d.ts +0 -34
  10. package/dist/editor-launch.js +0 -70
  11. package/dist/keymaps/emacs.d.ts +0 -18
  12. package/dist/keymaps/emacs.js +0 -133
  13. package/dist/keymaps/standard.d.ts +0 -17
  14. package/dist/keymaps/standard.js +0 -131
  15. package/dist/keymaps/types.d.ts +0 -43
  16. package/dist/keymaps/types.js +0 -28
  17. package/dist/keymaps/vim.d.ts +0 -48
  18. package/dist/keymaps/vim.js +0 -254
  19. package/dist/launch.d.ts +0 -149
  20. package/dist/launch.js +0 -433
  21. package/dist/sse.d.ts +0 -101
  22. package/dist/sse.js +0 -374
  23. package/dist/theme.d.ts +0 -51
  24. package/dist/theme.js +0 -105
  25. package/dist/ui/ambient.d.ts +0 -81
  26. package/dist/ui/ambient.js +0 -301
  27. package/dist/ui/app.d.ts +0 -209
  28. package/dist/ui/app.js +0 -1241
  29. package/dist/ui/composer.d.ts +0 -66
  30. package/dist/ui/composer.js +0 -125
  31. package/dist/ui/conversation-pane.d.ts +0 -123
  32. package/dist/ui/conversation-pane.js +0 -463
  33. package/dist/ui/help.d.ts +0 -15
  34. package/dist/ui/help.js +0 -80
  35. package/dist/ui/interactions.d.ts +0 -90
  36. package/dist/ui/interactions.js +0 -335
  37. package/dist/ui/picker.d.ts +0 -26
  38. package/dist/ui/picker.js +0 -130
  39. package/dist/ui/sidebar.d.ts +0 -76
  40. package/dist/ui/sidebar.js +0 -205
  41. package/dist/ui/status-bar.d.ts +0 -77
  42. package/dist/ui/status-bar.js +0 -146
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-10-27
11
+ ### Added
12
+ - Block rendering extended for the U5/U6/U7 workstreams: chat-block
13
+ transcripts, interaction and ambient surfaces rendered from block trees,
14
+ settings forms on the live surface revision, and the block transports.
15
+
16
+ ## [0.1.1] - 2026-10-24
17
+ ### Added
18
+ - Extension surfaces and live surface updates.
19
+ - Typed block rendering: content, braille charts, collections, forms,
20
+ page shells, images, navigation, and all three keymaps.
21
+
10
22
  ## [0.1.0] - 2026-10-21
11
23
  ### Added
12
24
  - First published release of the TUI (workspace package of
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigrun/tui",
3
- "version": "0.1.0",
4
- "description": "sigrun terminal client (pi-tui based) over the §5 API — spawned by the sigrun CLI, or standalone against a server URL",
3
+ "version": "0.2.0",
4
+ "description": "sigrun terminal client (pi-tui based) over the \u00a75 API \u2014 spawned by the sigrun CLI, or standalone against a server URL",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "sigrun-tui": "dist/launch.js"
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@earendil-works/pi-tui": "^1.3.0",
23
- "@sigrun/shared": "0.1.0"
23
+ "@sigrun/shared": "0.2.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "typescript": "^7.0.2",
package/dist/actions.d.ts DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * The TUI action layer (Task C3, UI refresh): a channel-neutral union of
3
- * everything the keymaps, pickers, mouse regions, and slash commands can
4
- * ask the shell to do. {@link TuiApp} implements {@link ActionHost} and
5
- * owns the focus state the focus/move/open actions drive.
6
- *
7
- * Focus targets: `composer` (the pi-tui Editor), `list` (the sidebar's
8
- * conversation tree), `transcript` (the pane's focusable turns). The
9
- * keymap decides WHICH action a key means per focus; the app decides
10
- * what the action DOES per focus.
11
- */
12
- /** The three focusable regions. */
13
- export type Focus = "composer" | "list" | "transcript";
14
- /** Every dispatchable intent. */
15
- export type Action = {
16
- readonly type: "focus";
17
- readonly target: Focus;
18
- } | {
19
- readonly type: "focus-cycle";
20
- readonly direction: 1 | -1;
21
- } | {
22
- readonly type: "move";
23
- readonly delta: number;
24
- } | {
25
- readonly type: "page";
26
- readonly direction: 1 | -1;
27
- readonly half: boolean;
28
- } | {
29
- readonly type: "edge";
30
- readonly to: "top" | "bottom";
31
- } | {
32
- readonly type: "prompt-jump";
33
- readonly delta: 1 | -1;
34
- } | {
35
- readonly type: "toggle-fold";
36
- } | {
37
- readonly type: "open";
38
- } | {
39
- readonly type: "copy";
40
- } | {
41
- readonly type: "fork-here";
42
- } | {
43
- readonly type: "edit-here";
44
- } | {
45
- readonly type: "edit-in-vim";
46
- } | {
47
- readonly type: "picker";
48
- readonly kind: "conversation" | "model" | "command";
49
- } | {
50
- readonly type: "new-conversation";
51
- } | {
52
- readonly type: "cycle-thinking";
53
- } | {
54
- readonly type: "search";
55
- readonly direction: "forward" | "backward";
56
- } | {
57
- readonly type: "search-next";
58
- readonly reverse: boolean;
59
- } | {
60
- readonly type: "answer-interaction";
61
- } | {
62
- readonly type: "cancel-run";
63
- } | {
64
- readonly type: "redraw";
65
- } | {
66
- readonly type: "quit";
67
- } | {
68
- readonly type: "help";
69
- } | {
70
- readonly type: "command-line";
71
- } | {
72
- readonly type: "run-command";
73
- readonly line: string;
74
- };
75
- /** The dispatch surface {@link TuiApp} implements. */
76
- export interface ActionHost {
77
- dispatch(action: Action): Promise<void>;
78
- }
package/dist/actions.js DELETED
@@ -1,12 +0,0 @@
1
- /**
2
- * The TUI action layer (Task C3, UI refresh): a channel-neutral union of
3
- * everything the keymaps, pickers, mouse regions, and slash commands can
4
- * ask the shell to do. {@link TuiApp} implements {@link ActionHost} and
5
- * owns the focus state the focus/move/open actions drive.
6
- *
7
- * Focus targets: `composer` (the pi-tui Editor), `list` (the sidebar's
8
- * conversation tree), `transcript` (the pane's focusable turns). The
9
- * keymap decides WHICH action a key means per focus; the app decides
10
- * what the action DOES per focus.
11
- */
12
- export {};
package/dist/api.d.ts DELETED
@@ -1,147 +0,0 @@
1
- /**
2
- * The TUI's typed §5 HTTP client (Task 4).
3
- *
4
- * Wire shapes are the channel contract — imported from the
5
- * `@sigrun/shared` package (`views`, plus `SessionMeta`/`SlashCommand`
6
- * siblings), the single source of truth shared with the server and the
7
- * webui (build-and-webui Task 1: the tui is a pure client of the wire,
8
- * never a relative importer of server source).
9
- *
10
- * Identity seam (Task 2): the configured `user` rides EVERY request as the
11
- * `X-Sigrun-User` header; absent/empty user omits it entirely — the
12
- * server's naive mode. The bearer token (Task 7) rides every request as
13
- * `authorization: Bearer <token>` when configured.
14
- *
15
- * Fail-loud: a non-2xx rejects with a {@link SigrunApiError} carrying the
16
- * status and the server's parsed error body (`{error: ...}`), never a
17
- * silent `undefined` the UI could misrender as empty state.
18
- */
19
- import type { CommandRunResponse, CommandsResponse } from "@sigrun/shared/commands";
20
- import type { JobsResponse, JobRunResponse } from "@sigrun/shared/views";
21
- import type { JobRevisionWire, JobRunWire } from "@sigrun/shared/jobs";
22
- import type { SessionMeta, SessionMetaPatch } from "@sigrun/shared/session-meta";
23
- import type { AbortResponse, AmbientList, CompactResponse, ConfigureResponse, ConversationCreateResponse, ConversationForkResponse, ConversationList, ConversationViewWire, InteractionAnswered, InteractionList, ModelsResponse, SettingsResponse, SubmissionResponse } from "@sigrun/shared/views";
24
- export type { AbortResponse, AmbientList, AmbientWire, CompactResponse, ConfigureResponse, ConversationCreateResponse, ConversationForkResponse, ConversationList, ConversationSummary, ConversationViewWire, InteractionAnswered, InteractionList, InteractionWire, ModelEntry, ModelsResponse, SettingsResponse, SubmissionResponse, } from "@sigrun/shared/views";
25
- export type { CommandRunResponse, CommandsResponse, SlashCommand } from "@sigrun/shared/commands";
26
- export type { JobSpec, JobSchedule, JobAction, JobWire, JobRunWire, JobRevisionWire, } from "@sigrun/shared/jobs";
27
- export type { JobsResponse, JobRunResponse } from "@sigrun/shared/views";
28
- export type { SessionMeta, SessionMetaPatch } from "@sigrun/shared/session-meta";
29
- /** One answer to a pending interaction (the ONE generic route's body). */
30
- export interface InteractionAnswerInput {
31
- readonly ids?: readonly string[];
32
- readonly values?: Readonly<Record<string, string | number | boolean>>;
33
- readonly text?: string;
34
- }
35
- /** `GET /v1/jobs/:id/runs`: the job's runs, newest first. */
36
- export interface JobRunsResponse {
37
- readonly runs: readonly JobRunWire[];
38
- }
39
- /** `GET /v1/jobs/:id/revisions`: the job's revisions, newest first. */
40
- export interface JobRevisionsResponse {
41
- readonly revisions: readonly JobRevisionWire[];
42
- }
43
- /** The `whenBusy` admission modes the server accepts (spec §7 item 3). */
44
- export type WhenBusyMode = "steer" | "followUp" | "reject";
45
- /** A non-2xx response: status + the server's parsed error body. */
46
- export declare class SigrunApiError extends Error {
47
- readonly status: number;
48
- readonly body: unknown;
49
- constructor(method: string, path: string, status: number, body: unknown);
50
- }
51
- /** The fetch surface the client needs (the real thing or a test stub). */
52
- export type FetchLike = (input: string, init?: {
53
- readonly method?: string;
54
- readonly headers?: Record<string, string>;
55
- readonly body?: string;
56
- }) => Promise<{
57
- readonly ok: boolean;
58
- readonly status: number;
59
- json(): Promise<unknown>;
60
- text(): Promise<string>;
61
- }>;
62
- export interface SigrunClientOptions {
63
- /**
64
- * The server's origin (e.g. `http://127.0.0.1:PORT`) — the TUI is a
65
- * remote client, so unlike the webui there is no same-origin default.
66
- */
67
- readonly baseUrl: string;
68
- /**
69
- * The `X-Sigrun-User` identity (Task 2). Absent/empty/whitespace → the
70
- * header is omitted entirely (the server's naive mode).
71
- */
72
- readonly user?: string;
73
- /**
74
- * A bearer token (Task 7): rides EVERY request as
75
- * `authorization: Bearer <token>`. Absent/empty/whitespace → omitted.
76
- * The launcher threads `SIGRUN_TOKEN` here — the env beats tui.json.
77
- */
78
- readonly token?: string;
79
- /** Injected for tests; defaults to the ambient `fetch`. */
80
- readonly fetch?: FetchLike;
81
- }
82
- /** The typed §5 client. See {@link SigrunClientOptions} for the seams. */
83
- export declare class SigrunClient {
84
- #private;
85
- constructor(options: SigrunClientOptions);
86
- /** `GET /v1/conversations` — all durable conversations (Task 3). */
87
- listConversations(): Promise<ConversationList>;
88
- /** `GET /v1/conversations/:id` — the full wire view (one frame). */
89
- getConversation(id: number): Promise<ConversationViewWire>;
90
- /** `POST /v1/conversations` — the new id plus its first view frame. */
91
- createConversation(): Promise<ConversationCreateResponse>;
92
- /** `POST /v1/conversations/:id/fork` (absent `at` = the newest entry). */
93
- forkConversation(id: number, at?: number): Promise<ConversationForkResponse>;
94
- /**
95
- * `POST /v1/conversations/:id/submissions` — plain text input; 202 at
96
- * ADMISSION (§7.1) with the record as it stands. The TUI is
97
- * fire-and-forget: the reply arrives as view revisions on the stream,
98
- * not from this response.
99
- */
100
- submit(id: number, text: string, whenBusy?: WhenBusyMode): Promise<SubmissionResponse>;
101
- /** `POST /v1/conversations/:id/abort`. */
102
- abort(id: number): Promise<AbortResponse>;
103
- /**
104
- * `POST /v1/conversations/:id/compact` — admits a manual compaction;
105
- * the summary entry arrives later as a view revision on the stream.
106
- */
107
- compact(id: number, instructions?: string): Promise<CompactResponse>;
108
- /** `GET /v1/conversations/:id/meta`. */
109
- getMeta(id: number): Promise<SessionMeta>;
110
- /** `PATCH /v1/conversations/:id/meta` — merges; resolves to the new meta. */
111
- patchMeta(id: number, patch: SessionMetaPatch): Promise<SessionMeta>;
112
- /** `GET /v1/conversations/:id/interactions` — pending interactions. */
113
- listInteractions(id: number): Promise<InteractionList>;
114
- /** `POST .../interactions/:callId/answer` — the ONE generic answer route. */
115
- answerInteraction(id: number, callId: string, answer: InteractionAnswerInput): Promise<InteractionAnswered>;
116
- /** `GET /v1/conversations/:id/ambient`. */
117
- getAmbient(id: number): Promise<AmbientList>;
118
- /** `GET /v1/commands` — the slash-command catalog (a boot constant). */
119
- getCommands(): Promise<CommandsResponse>;
120
- /**
121
- * `POST /v1/conversations/:id/commands/:name` — invoke an extension
122
- * slash command (W5 Task 3): the server runs the winning declaration
123
- * and resolves with its optional `{notice}`.
124
- */
125
- runCommand(id: number, name: string, args?: string): Promise<CommandRunResponse>;
126
- /** `GET /v1/settings` — the effective settings (absent file = `{}`). */
127
- getSettings(): Promise<SettingsResponse>;
128
- /** `GET /v1/models` — the chat-model catalog (the model picker's items). */
129
- listModels(): Promise<ModelsResponse>;
130
- /**
131
- * `POST /v1/conversations/:id/configure` — change the model and/or
132
- * thinking level (T9c-i). At least one field is required (the server
133
- * rejects an empty change as `invalid_body`).
134
- */
135
- configure(id: number, body: {
136
- readonly model?: string;
137
- readonly thinking?: string;
138
- }): Promise<ConfigureResponse>;
139
- /** `GET /v1/jobs` — the caller-visible jobs (`/jobs`, W4 Task 5). */
140
- listJobs(): Promise<JobsResponse>;
141
- /** `POST /v1/jobs/:id/run` — fire a job now (write access). */
142
- runJob(id: string): Promise<JobRunResponse>;
143
- /** `GET /v1/jobs/:id/runs` — the job's runs, newest first. */
144
- listJobRuns(id: string, limit?: number): Promise<JobRunsResponse>;
145
- /** `GET /v1/jobs/:id/revisions` — the job's revisions, newest first. */
146
- listJobRevisions(id: string): Promise<JobRevisionsResponse>;
147
- }
package/dist/api.js DELETED
@@ -1,185 +0,0 @@
1
- /**
2
- * The TUI's typed §5 HTTP client (Task 4).
3
- *
4
- * Wire shapes are the channel contract — imported from the
5
- * `@sigrun/shared` package (`views`, plus `SessionMeta`/`SlashCommand`
6
- * siblings), the single source of truth shared with the server and the
7
- * webui (build-and-webui Task 1: the tui is a pure client of the wire,
8
- * never a relative importer of server source).
9
- *
10
- * Identity seam (Task 2): the configured `user` rides EVERY request as the
11
- * `X-Sigrun-User` header; absent/empty user omits it entirely — the
12
- * server's naive mode. The bearer token (Task 7) rides every request as
13
- * `authorization: Bearer <token>` when configured.
14
- *
15
- * Fail-loud: a non-2xx rejects with a {@link SigrunApiError} carrying the
16
- * status and the server's parsed error body (`{error: ...}`), never a
17
- * silent `undefined` the UI could misrender as empty state.
18
- */
19
- /** A non-2xx response: status + the server's parsed error body. */
20
- export class SigrunApiError extends Error {
21
- status;
22
- body;
23
- constructor(method, path, status, body) {
24
- super(`sigrun api: ${method} ${path} failed with status ${status}`);
25
- this.name = "SigrunApiError";
26
- this.status = status;
27
- this.body = body;
28
- }
29
- }
30
- /** The typed §5 client. See {@link SigrunClientOptions} for the seams. */
31
- export class SigrunClient {
32
- #baseUrl;
33
- #fetch;
34
- #headers;
35
- constructor(options) {
36
- // A trailing slash would double every route's leading one
37
- // (`--server http://host/` → `//v1/...`) — normalize once.
38
- this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
39
- this.#fetch =
40
- options.fetch ??
41
- ((input, init) => globalThis.fetch(input, init));
42
- const user = options.user?.trim();
43
- const token = options.token?.trim();
44
- this.#headers = {
45
- accept: "application/json",
46
- ...(user === undefined || user === "" ? {} : { "x-sigrun-user": user }),
47
- ...(token === undefined || token === ""
48
- ? {}
49
- : { authorization: `Bearer ${token}` }),
50
- };
51
- }
52
- async #get(path) {
53
- const response = await this.#fetch(`${this.#baseUrl}${path}`, {
54
- method: "GET",
55
- headers: this.#headers,
56
- });
57
- if (!response.ok) {
58
- throw new SigrunApiError("GET", path, response.status, await response.json().catch(() => undefined));
59
- }
60
- return (await response.json());
61
- }
62
- async #send(method, path, body) {
63
- const response = await this.#fetch(`${this.#baseUrl}${path}`, {
64
- method,
65
- headers: { ...this.#headers, "content-type": "application/json" },
66
- body: JSON.stringify(body),
67
- });
68
- if (!response.ok) {
69
- throw new SigrunApiError(method, path, response.status, await response.json().catch(() => undefined));
70
- }
71
- return (await response.json());
72
- }
73
- /** `GET /v1/conversations` — all durable conversations (Task 3). */
74
- listConversations() {
75
- return this.#get("/v1/conversations");
76
- }
77
- /** `GET /v1/conversations/:id` — the full wire view (one frame). */
78
- getConversation(id) {
79
- return this.#get(`/v1/conversations/${id}`);
80
- }
81
- /** `POST /v1/conversations` — the new id plus its first view frame. */
82
- createConversation() {
83
- return this.#send("POST", "/v1/conversations", {});
84
- }
85
- /** `POST /v1/conversations/:id/fork` (absent `at` = the newest entry). */
86
- forkConversation(id, at) {
87
- return this.#send("POST", `/v1/conversations/${id}/fork`, {
88
- ...(at === undefined ? {} : { at }),
89
- });
90
- }
91
- /**
92
- * `POST /v1/conversations/:id/submissions` — plain text input; 202 at
93
- * ADMISSION (§7.1) with the record as it stands. The TUI is
94
- * fire-and-forget: the reply arrives as view revisions on the stream,
95
- * not from this response.
96
- */
97
- submit(id, text, whenBusy) {
98
- return this.#send("POST", `/v1/conversations/${id}/submissions`, {
99
- input: [{ type: "text", text }],
100
- ...(whenBusy === undefined ? {} : { whenBusy }),
101
- });
102
- }
103
- /** `POST /v1/conversations/:id/abort`. */
104
- abort(id) {
105
- return this.#send("POST", `/v1/conversations/${id}/abort`, {});
106
- }
107
- /**
108
- * `POST /v1/conversations/:id/compact` — admits a manual compaction;
109
- * the summary entry arrives later as a view revision on the stream.
110
- */
111
- compact(id, instructions) {
112
- return this.#send("POST", `/v1/conversations/${id}/compact`, {
113
- ...(instructions === undefined || instructions === ""
114
- ? {}
115
- : { instructions }),
116
- });
117
- }
118
- /** `GET /v1/conversations/:id/meta`. */
119
- getMeta(id) {
120
- return this.#get(`/v1/conversations/${id}/meta`);
121
- }
122
- /** `PATCH /v1/conversations/:id/meta` — merges; resolves to the new meta. */
123
- patchMeta(id, patch) {
124
- return this.#send("PATCH", `/v1/conversations/${id}/meta`, patch);
125
- }
126
- /** `GET /v1/conversations/:id/interactions` — pending interactions. */
127
- listInteractions(id) {
128
- return this.#get(`/v1/conversations/${id}/interactions`);
129
- }
130
- /** `POST .../interactions/:callId/answer` — the ONE generic answer route. */
131
- answerInteraction(id, callId, answer) {
132
- return this.#send("POST", `/v1/conversations/${id}/interactions/${encodeURIComponent(callId)}/answer`, answer);
133
- }
134
- /** `GET /v1/conversations/:id/ambient`. */
135
- getAmbient(id) {
136
- return this.#get(`/v1/conversations/${id}/ambient`);
137
- }
138
- /** `GET /v1/commands` — the slash-command catalog (a boot constant). */
139
- getCommands() {
140
- return this.#get("/v1/commands");
141
- }
142
- /**
143
- * `POST /v1/conversations/:id/commands/:name` — invoke an extension
144
- * slash command (W5 Task 3): the server runs the winning declaration
145
- * and resolves with its optional `{notice}`.
146
- */
147
- runCommand(id, name, args) {
148
- return this.#send("POST", `/v1/conversations/${id}/commands/${encodeURIComponent(name)}`, {
149
- args: args ?? "",
150
- });
151
- }
152
- /** `GET /v1/settings` — the effective settings (absent file = `{}`). */
153
- getSettings() {
154
- return this.#get("/v1/settings");
155
- }
156
- /** `GET /v1/models` — the chat-model catalog (the model picker's items). */
157
- listModels() {
158
- return this.#get("/v1/models");
159
- }
160
- /**
161
- * `POST /v1/conversations/:id/configure` — change the model and/or
162
- * thinking level (T9c-i). At least one field is required (the server
163
- * rejects an empty change as `invalid_body`).
164
- */
165
- configure(id, body) {
166
- return this.#send("POST", `/v1/conversations/${id}/configure`, body);
167
- }
168
- /** `GET /v1/jobs` — the caller-visible jobs (`/jobs`, W4 Task 5). */
169
- listJobs() {
170
- return this.#get("/v1/jobs");
171
- }
172
- /** `POST /v1/jobs/:id/run` — fire a job now (write access). */
173
- runJob(id) {
174
- return this.#send("POST", `/v1/jobs/${encodeURIComponent(id)}/run`, {});
175
- }
176
- /** `GET /v1/jobs/:id/runs` — the job's runs, newest first. */
177
- listJobRuns(id, limit) {
178
- return this.#get(`/v1/jobs/${encodeURIComponent(id)}/runs` +
179
- (limit === undefined ? "" : `?limit=${limit}`));
180
- }
181
- /** `GET /v1/jobs/:id/revisions` — the job's revisions, newest first. */
182
- listJobRevisions(id) {
183
- return this.#get(`/v1/jobs/${encodeURIComponent(id)}/revisions`);
184
- }
185
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Terminal clipboard (Task C3, UI refresh): OSC 52 writes the system
3
- * clipboard through the terminal emulator. Inside tmux the sequence must
4
- * be DCS-wrapped with every ESC doubled, or tmux eats it.
5
- *
6
- * `copyToTerminal` writes the sequence to the terminal; the app flashes
7
- * "Copied" in the status bar on top of this.
8
- */
9
- /** The environment slice the tmux detection reads. */
10
- export interface ClipboardEnv {
11
- readonly TMUX?: string;
12
- }
13
- /** The OSC 52 sequence for `text` (base64 UTF-8), tmux-wrapped when needed. */
14
- export declare function osc52(text: string, env?: ClipboardEnv): string;
15
- /** The terminal surface the copy needs (pi-tui's `Terminal.write`). */
16
- export interface ClipboardTerminal {
17
- write(data: string): void;
18
- }
19
- /** Write `text` to the system clipboard via the terminal. */
20
- export declare function copyToTerminal(terminal: ClipboardTerminal, text: string, env?: ClipboardEnv): void;
package/dist/clipboard.js DELETED
@@ -1,19 +0,0 @@
1
- /**
2
- * Terminal clipboard (Task C3, UI refresh): OSC 52 writes the system
3
- * clipboard through the terminal emulator. Inside tmux the sequence must
4
- * be DCS-wrapped with every ESC doubled, or tmux eats it.
5
- *
6
- * `copyToTerminal` writes the sequence to the terminal; the app flashes
7
- * "Copied" in the status bar on top of this.
8
- */
9
- /** The OSC 52 sequence for `text` (base64 UTF-8), tmux-wrapped when needed. */
10
- export function osc52(text, env = process.env) {
11
- const seq = `\x1b]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`;
12
- if (env.TMUX === undefined || env.TMUX === "")
13
- return seq;
14
- return `\x1bPtmux;\x1b${seq.replaceAll("\x1b", "\x1b\x1b")}\x1b\\`;
15
- }
16
- /** Write `text` to the system clipboard via the terminal. */
17
- export function copyToTerminal(terminal, text, env = process.env) {
18
- terminal.write(osc52(text, env));
19
- }
@@ -1,34 +0,0 @@
1
- export declare const VIM_NAMES: readonly string[];
2
- /** True when the basename of the first whitespace-separated token is a vim binary. */
3
- export declare function isVimFamily(command: string): boolean;
4
- /**
5
- * Resolve a vim-family editor: configured → VISUAL → EDITOR (each used only
6
- * when vim-family), then nvim/vim/vi on PATH. Undefined when nothing qualifies.
7
- */
8
- export declare function resolveVimEditor(opts: {
9
- configured?: string;
10
- env: NodeJS.ProcessEnv;
11
- which(name: string): string | undefined;
12
- }): string | undefined;
13
- /**
14
- * Edit `text` in a vim-family editor via a temp file, suspending the TUI for
15
- * the duration. The temp file is always removed; on failure the draft is
16
- * untouched and stop/start remain balanced.
17
- */
18
- export declare function editInVim(opts: {
19
- text: string;
20
- editor: string;
21
- tui: {
22
- stop(): void;
23
- start(): void;
24
- requestRender(force?: boolean): void;
25
- };
26
- spawnSync?: typeof import("node:child_process").spawnSync;
27
- tmpDir?: string;
28
- }): {
29
- ok: true;
30
- text: string;
31
- } | {
32
- ok: false;
33
- reason: string;
34
- };
@@ -1,70 +0,0 @@
1
- import { spawnSync as realSpawnSync } from "node:child_process";
2
- import { randomBytes } from "node:crypto";
3
- import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { basename, join } from "node:path";
6
- export const VIM_NAMES = ["vim", "nvim", "vi"];
7
- /** True when the basename of the first whitespace-separated token is a vim binary. */
8
- export function isVimFamily(command) {
9
- const first = command.trim().split(/\s+/)[0];
10
- if (!first)
11
- return false;
12
- return VIM_NAMES.includes(basename(first));
13
- }
14
- /**
15
- * Resolve a vim-family editor: configured → VISUAL → EDITOR (each used only
16
- * when vim-family), then nvim/vim/vi on PATH. Undefined when nothing qualifies.
17
- */
18
- export function resolveVimEditor(opts) {
19
- for (const candidate of [opts.configured, opts.env.VISUAL, opts.env.EDITOR]) {
20
- if (candidate && isVimFamily(candidate))
21
- return candidate;
22
- }
23
- for (const name of ["nvim", "vim", "vi"]) {
24
- const found = opts.which(name);
25
- if (found)
26
- return found;
27
- }
28
- return undefined;
29
- }
30
- /**
31
- * Edit `text` in a vim-family editor via a temp file, suspending the TUI for
32
- * the duration. The temp file is always removed; on failure the draft is
33
- * untouched and stop/start remain balanced.
34
- */
35
- export function editInVim(opts) {
36
- const spawn = opts.spawnSync ?? realSpawnSync;
37
- const file = join(opts.tmpDir ?? tmpdir(), `sigrun-prompt-${randomBytes(8).toString("hex")}.md`);
38
- writeFileSync(file, opts.text, { encoding: "utf8", mode: 0o600 });
39
- // The editor string splits on whitespace — quoted paths with spaces in
40
- // the command or its arguments are NOT supported (a known limitation).
41
- const [command, ...args] = opts.editor.trim().split(/\s+/);
42
- try {
43
- opts.tui.stop();
44
- const result = spawn(command, [...args, file], { stdio: "inherit" });
45
- if (result.error) {
46
- return { ok: false, reason: `editor failed to launch: ${result.error.message}` };
47
- }
48
- if (result.status !== 0) {
49
- return { ok: false, reason: `editor exited with status ${result.status}` };
50
- }
51
- let text;
52
- try {
53
- text = readFileSync(file, "utf8");
54
- }
55
- catch {
56
- return { ok: false, reason: "edited file missing or unreadable" };
57
- }
58
- return { ok: true, text };
59
- }
60
- finally {
61
- opts.tui.start();
62
- opts.tui.requestRender(true);
63
- try {
64
- unlinkSync(file);
65
- }
66
- catch {
67
- // already gone (editor deleted it) — nothing to clean up
68
- }
69
- }
70
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * The emacs keymap (Task C5, UI refresh): modeless, chord-driven. `C-x`
3
- * ("\x18") arms a 1 000 ms chord: `C-x b` conversation picker, `C-x o`
4
- * focus cycle, `C-x C-c` quit, `C-x k` a consumed no-op (a mismatched
5
- * follow-up is the emacs undefined-key beep — consumed, never typed).
6
- * `M-x` opens the command picker; `C-s` / `C-r` search forward / backward
7
- * (the app's search overlay turns them into next / previous while open);
8
- * `C-g` cancels a live run, closes the search/picker (it passes through
9
- * to the open overlay), or focuses the composer. `M-p` / `M-n` jump
10
- * between prompts; `M-w` copy, `M-f` fork, `M-e` edit are outside-
11
- * composer actions (in the composer `M-f` is the editor's word-forward —
12
- * all three pass). `C-n` / `C-p` move outside the composer; `C-o`
13
- * answers an interaction, `C-l` redraws, `C-h` opens the key reference.
14
- * Every {@link RESERVED_KEYS} chord passes in every focus — ctrl+b is
15
- * NEVER bound (no `C-x C-b` either).
16
- */
17
- import { type Keymap } from "./types.js";
18
- export declare function createEmacsKeymap(): Keymap;