@duckmind/dm-darwin-x64 0.13.6 → 0.13.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.
Files changed (78) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +26 -2
  3. package/extensions/dm-phone/README.md +23 -0
  4. package/extensions/dm-phone/index.ts +12 -0
  5. package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
  6. package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
  7. package/extensions/dm-phone/node_modules/ws/README.md +548 -0
  8. package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
  9. package/extensions/dm-phone/node_modules/ws/index.js +22 -0
  10. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
  11. package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
  12. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
  13. package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
  14. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
  15. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
  16. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
  17. package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
  18. package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
  19. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
  20. package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
  21. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
  22. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
  23. package/extensions/dm-phone/node_modules/ws/package.json +70 -0
  24. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
  25. package/extensions/dm-phone/package-lock.json +66 -0
  26. package/extensions/dm-phone/package.json +35 -0
  27. package/extensions/dm-phone/phone-session-pool.ts +8 -0
  28. package/extensions/dm-phone/public/app/attachments.js +233 -0
  29. package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
  30. package/extensions/dm-phone/public/app/autocomplete.js +135 -0
  31. package/extensions/dm-phone/public/app/bindings.js +178 -0
  32. package/extensions/dm-phone/public/app/command-catalog.js +76 -0
  33. package/extensions/dm-phone/public/app/commands.js +370 -0
  34. package/extensions/dm-phone/public/app/constants.js +60 -0
  35. package/extensions/dm-phone/public/app/formatters.js +131 -0
  36. package/extensions/dm-phone/public/app/handlers.js +442 -0
  37. package/extensions/dm-phone/public/app/main.js +6 -0
  38. package/extensions/dm-phone/public/app/markdown.js +105 -0
  39. package/extensions/dm-phone/public/app/messages.js +418 -0
  40. package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
  41. package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
  42. package/extensions/dm-phone/public/app/sheets-view.js +272 -0
  43. package/extensions/dm-phone/public/app/state.js +95 -0
  44. package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
  45. package/extensions/dm-phone/public/app/transport.js +176 -0
  46. package/extensions/dm-phone/public/app/ui.js +409 -0
  47. package/extensions/dm-phone/public/app.js +1 -0
  48. package/extensions/dm-phone/public/icon.svg +15 -0
  49. package/extensions/dm-phone/public/index.html +147 -0
  50. package/extensions/dm-phone/public/manifest.webmanifest +17 -0
  51. package/extensions/dm-phone/public/styles.css +1139 -0
  52. package/extensions/dm-phone/public/sw.js +78 -0
  53. package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
  54. package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
  55. package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
  56. package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
  57. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
  58. package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
  59. package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
  60. package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
  61. package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
  62. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
  63. package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
  64. package/extensions/dm-phone/src/extension/types.ts +73 -0
  65. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
  66. package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
  67. package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
  68. package/extensions/dm-phone/src/session-pool/types.ts +105 -0
  69. package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
  70. package/extensions/dm-subagents/agent-management.ts +15 -6
  71. package/extensions/dm-subagents/agent-manager-detail.ts +12 -2
  72. package/extensions/dm-subagents/agent-manager-edit.ts +75 -23
  73. package/extensions/dm-subagents/agent-manager-list.ts +9 -2
  74. package/extensions/dm-subagents/agent-manager.ts +199 -11
  75. package/extensions/dm-subagents/agents.ts +315 -20
  76. package/extensions/dm-ultrathink/README.md +5 -0
  77. package/extensions/dm-ultrathink/src/naming.ts +75 -3
  78. package/package.json +1 -1
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "ws",
3
+ "version": "8.20.0",
4
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
+ "keywords": [
6
+ "HyBi",
7
+ "Push",
8
+ "RFC-6455",
9
+ "WebSocket",
10
+ "WebSockets",
11
+ "real-time"
12
+ ],
13
+ "homepage": "https://github.com/websockets/ws",
14
+ "bugs": "https://github.com/websockets/ws/issues",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/websockets/ws.git"
18
+ },
19
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
20
+ "license": "MIT",
21
+ "main": "index.js",
22
+ "exports": {
23
+ ".": {
24
+ "browser": "./browser.js",
25
+ "import": "./wrapper.mjs",
26
+ "require": "./index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "browser": "browser.js",
31
+ "engines": {
32
+ "node": ">=10.0.0"
33
+ },
34
+ "files": [
35
+ "browser.js",
36
+ "index.js",
37
+ "lib/*.js",
38
+ "wrapper.mjs"
39
+ ],
40
+ "scripts": {
41
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
42
+ "integration": "mocha --throw-deprecation test/*.integration.js",
43
+ "lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
44
+ },
45
+ "peerDependencies": {
46
+ "bufferutil": "^4.0.1",
47
+ "utf-8-validate": ">=5.0.2"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "bufferutil": {
51
+ "optional": true
52
+ },
53
+ "utf-8-validate": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
59
+ "benchmark": "^2.1.4",
60
+ "bufferutil": "^4.0.1",
61
+ "eslint": "^10.0.1",
62
+ "eslint-config-prettier": "^10.0.1",
63
+ "eslint-plugin-prettier": "^5.0.0",
64
+ "globals": "^17.0.0",
65
+ "mocha": "^8.4.0",
66
+ "nyc": "^15.0.0",
67
+ "prettier": "^3.0.0",
68
+ "utf-8-validate": "^6.0.0"
69
+ }
70
+ }
@@ -0,0 +1,21 @@
1
+ import createWebSocketStream from './lib/stream.js';
2
+ import extension from './lib/extension.js';
3
+ import PerMessageDeflate from './lib/permessage-deflate.js';
4
+ import Receiver from './lib/receiver.js';
5
+ import Sender from './lib/sender.js';
6
+ import subprotocol from './lib/subprotocol.js';
7
+ import WebSocket from './lib/websocket.js';
8
+ import WebSocketServer from './lib/websocket-server.js';
9
+
10
+ export {
11
+ createWebSocketStream,
12
+ extension,
13
+ PerMessageDeflate,
14
+ Receiver,
15
+ Sender,
16
+ subprotocol,
17
+ WebSocket,
18
+ WebSocketServer
19
+ };
20
+
21
+ export default WebSocket;
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "dm-phone",
3
+ "version": "0.0.13",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "dm-phone",
9
+ "version": "0.0.13",
10
+ "dependencies": {
11
+ "ws": "^8.18.3"
12
+ },
13
+ "devDependencies": {
14
+ "@types/ws": "^8.18.1"
15
+ }
16
+ },
17
+ "node_modules/@types/node": {
18
+ "version": "25.6.0",
19
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
20
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
21
+ "dev": true,
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "undici-types": "~7.19.0"
25
+ }
26
+ },
27
+ "node_modules/@types/ws": {
28
+ "version": "8.18.1",
29
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
30
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
31
+ "dev": true,
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@types/node": "*"
35
+ }
36
+ },
37
+ "node_modules/undici-types": {
38
+ "version": "7.19.2",
39
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
40
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
41
+ "dev": true,
42
+ "license": "MIT"
43
+ },
44
+ "node_modules/ws": {
45
+ "version": "8.20.0",
46
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
47
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
48
+ "license": "MIT",
49
+ "engines": {
50
+ "node": ">=10.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "bufferutil": "^4.0.1",
54
+ "utf-8-validate": ">=5.0.2"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "bufferutil": {
58
+ "optional": true
59
+ },
60
+ "utf-8-validate": {
61
+ "optional": true
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "dm-phone",
3
+ "version": "0.0.13",
4
+ "description": "Phone-first remote UI extension for DM with Tailscale-friendly access",
5
+ "type": "module",
6
+ "keywords": [
7
+ "duckmind",
8
+ "dm",
9
+ "extension",
10
+ "phone",
11
+ "mobile",
12
+ "tailscale"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "index.ts",
19
+ "phone-session-pool.ts",
20
+ "src",
21
+ "public",
22
+ "README.md"
23
+ ],
24
+ "pi": {
25
+ "extensions": [
26
+ "./index.ts"
27
+ ]
28
+ },
29
+ "dependencies": {
30
+ "ws": "^8.18.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/ws": "^8.18.1"
34
+ }
35
+ }
@@ -0,0 +1,8 @@
1
+ export { PhoneSessionPool } from "./src/session-pool/session-pool";
2
+ export { PhoneSessionWorker } from "./src/session-pool/session-worker";
3
+ export type {
4
+ PhoneSessionPoolOptions,
5
+ SessionSnapshot,
6
+ SessionSummary,
7
+ SessionWorkerOptions,
8
+ } from "./src/session-pool/types";
@@ -0,0 +1,233 @@
1
+ import { insertTextAtCursor } from "./autocomplete.js";
2
+ import { escapeHtml, formatBytes } from "./formatters.js";
3
+ import { el, state } from "./state.js";
4
+ import { autoResizeTextarea, scheduleComposerLayoutSync } from "./ui.js";
5
+
6
+ function currentPromptText() {
7
+ return String(el.promptInput?.value || "");
8
+ }
9
+
10
+ function createAttachmentRecord(file) {
11
+ const tokenOrder = state.nextAttachmentTokenId || 1;
12
+ state.nextAttachmentTokenId = tokenOrder + 1;
13
+
14
+ return {
15
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
16
+ file,
17
+ name: file.name,
18
+ size: file.size,
19
+ type: file.type,
20
+ url: URL.createObjectURL(file),
21
+ token: `⟦img${tokenOrder}⟧`,
22
+ tokenOrder,
23
+ };
24
+ }
25
+
26
+ function orderedAttachments(promptText = currentPromptText()) {
27
+ return [...state.attachments].sort((left, right) => {
28
+ const leftIndex = promptText.indexOf(left.token);
29
+ const rightIndex = promptText.indexOf(right.token);
30
+ const leftMissing = leftIndex === -1;
31
+ const rightMissing = rightIndex === -1;
32
+
33
+ if (leftMissing !== rightMissing) return leftMissing ? 1 : -1;
34
+ if (!leftMissing && leftIndex !== rightIndex) return leftIndex - rightIndex;
35
+ return (left.tokenOrder || 0) - (right.tokenOrder || 0);
36
+ });
37
+ }
38
+
39
+ function disposeAttachment(attachment) {
40
+ if (attachment?.url) {
41
+ URL.revokeObjectURL(attachment.url);
42
+ }
43
+ }
44
+
45
+ function disposeAttachments(attachments = []) {
46
+ for (const attachment of attachments) {
47
+ disposeAttachment(attachment);
48
+ }
49
+ }
50
+
51
+ function buildTokenInsertion(tokens) {
52
+ if (!tokens.length) return "";
53
+
54
+ const value = currentPromptText();
55
+ const start = el.promptInput.selectionStart ?? value.length;
56
+ const end = el.promptInput.selectionEnd ?? start;
57
+ const beforeChar = start > 0 ? value[start - 1] : "";
58
+ const afterChar = end < value.length ? value[end] : "";
59
+ const needsLeadingSpace = beforeChar && !/\s/.test(beforeChar);
60
+ const needsTrailingSpace = !afterChar || !/\s/.test(afterChar);
61
+
62
+ return `${needsLeadingSpace ? " " : ""}${tokens.join(" ")}${needsTrailingSpace ? " " : ""}`;
63
+ }
64
+
65
+ function stripTokenFromPrompt(token) {
66
+ const value = currentPromptText();
67
+ if (!token || !value.includes(token)) return false;
68
+
69
+ const selectionStart = el.promptInput.selectionStart ?? value.length;
70
+ const selectionEnd = el.promptInput.selectionEnd ?? selectionStart;
71
+ let nextText = "";
72
+ let nextSelectionStart = selectionStart;
73
+ let nextSelectionEnd = selectionEnd;
74
+ let offset = 0;
75
+
76
+ while (offset < value.length) {
77
+ const index = value.indexOf(token, offset);
78
+ if (index === -1) break;
79
+
80
+ nextText += value.slice(offset, index);
81
+
82
+ if (index < nextSelectionStart) {
83
+ nextSelectionStart -= Math.min(token.length, nextSelectionStart - index);
84
+ }
85
+ if (index < nextSelectionEnd) {
86
+ nextSelectionEnd -= Math.min(token.length, nextSelectionEnd - index);
87
+ }
88
+
89
+ offset = index + token.length;
90
+ }
91
+
92
+ nextText += value.slice(offset);
93
+ if (nextText === value) return false;
94
+
95
+ el.promptInput.value = nextText;
96
+ const clampedStart = Math.max(0, Math.min(nextText.length, nextSelectionStart));
97
+ const clampedEnd = Math.max(clampedStart, Math.min(nextText.length, nextSelectionEnd));
98
+ el.promptInput.focus();
99
+ el.promptInput.setSelectionRange(clampedStart, clampedEnd);
100
+ autoResizeTextarea();
101
+ return true;
102
+ }
103
+
104
+ export function renderAttachmentStrip() {
105
+ const attachments = orderedAttachments();
106
+ if (!attachments.length) {
107
+ el.attachmentStrip.classList.add("hidden");
108
+ el.attachmentStrip.innerHTML = "";
109
+ scheduleComposerLayoutSync();
110
+ return;
111
+ }
112
+
113
+ el.attachmentStrip.innerHTML = attachments.map((attachment) => `
114
+ <article class="attachment-chip">
115
+ <img src="${attachment.url}" alt="${escapeHtml(attachment.name)}" />
116
+ <div class="attachment-chip-header">
117
+ <div class="attachment-chip-token mono">${escapeHtml(attachment.token)}</div>
118
+ <button class="attachment-chip-remove" data-remove-attachment="${attachment.id}" aria-label="Remove image ${escapeHtml(attachment.token)}">✕</button>
119
+ </div>
120
+ <div class="attachment-chip-name">${escapeHtml(attachment.name)}</div>
121
+ <div class="attachment-chip-meta">${escapeHtml(formatBytes(attachment.size))}</div>
122
+ </article>
123
+ `).join("");
124
+ el.attachmentStrip.classList.remove("hidden");
125
+ scheduleComposerLayoutSync();
126
+ }
127
+
128
+ export function addAttachments(files) {
129
+ const incoming = Array.from(files || []).filter((file) => file.type.startsWith("image/"));
130
+ if (!incoming.length) return;
131
+
132
+ const added = incoming.map((file) => createAttachmentRecord(file));
133
+ state.attachments.push(...added);
134
+
135
+ const insertion = buildTokenInsertion(added.map((attachment) => attachment.token));
136
+ if (insertion) {
137
+ insertTextAtCursor(insertion);
138
+ }
139
+
140
+ syncAttachmentsWithPrompt();
141
+ renderAttachmentStrip();
142
+ }
143
+
144
+ export function removeAttachment(id) {
145
+ const index = state.attachments.findIndex((attachment) => attachment.id === id);
146
+ if (index === -1) return;
147
+
148
+ const [removed] = state.attachments.splice(index, 1);
149
+ stripTokenFromPrompt(removed.token);
150
+ disposeAttachment(removed);
151
+ renderAttachmentStrip();
152
+ }
153
+
154
+ export function clearAttachments(options = {}) {
155
+ if (options.removeTokensFromPrompt) {
156
+ for (const attachment of state.attachments) {
157
+ stripTokenFromPrompt(attachment.token);
158
+ }
159
+ }
160
+
161
+ disposeAttachments(state.attachments);
162
+ state.attachments = [];
163
+ renderAttachmentStrip();
164
+ }
165
+
166
+ function fileToBase64(file) {
167
+ return new Promise((resolve, reject) => {
168
+ const reader = new FileReader();
169
+ reader.onload = () => {
170
+ const result = String(reader.result || "");
171
+ const base64 = result.includes(",") ? result.split(",")[1] : result;
172
+ resolve(base64);
173
+ };
174
+ reader.onerror = () => reject(reader.error || new Error(`Failed to read ${file.name}`));
175
+ reader.readAsDataURL(file);
176
+ });
177
+ }
178
+
179
+ export function syncAttachmentsWithPrompt() {
180
+ if (!state.attachments.length) return [];
181
+
182
+ const promptText = currentPromptText();
183
+ const kept = [];
184
+ const removed = [];
185
+
186
+ for (const attachment of state.attachments) {
187
+ if (promptText.includes(attachment.token)) {
188
+ kept.push(attachment);
189
+ } else {
190
+ removed.push(attachment);
191
+ }
192
+ }
193
+
194
+ if (removed.length) {
195
+ disposeAttachments(removed);
196
+ state.attachments = kept;
197
+ }
198
+
199
+ renderAttachmentStrip();
200
+ return removed;
201
+ }
202
+
203
+ function attachmentOccurrences(promptText = currentPromptText()) {
204
+ const matches = [];
205
+
206
+ for (const attachment of state.attachments) {
207
+ let offset = 0;
208
+ while (offset <= promptText.length) {
209
+ const index = promptText.indexOf(attachment.token, offset);
210
+ if (index === -1) break;
211
+ matches.push({ index, attachment });
212
+ offset = index + attachment.token.length;
213
+ }
214
+ }
215
+
216
+ matches.sort((left, right) => left.index - right.index || (left.attachment.tokenOrder || 0) - (right.attachment.tokenOrder || 0));
217
+ return matches.map((match) => match.attachment);
218
+ }
219
+
220
+ export async function buildPromptPayload(promptText = currentPromptText()) {
221
+ const rawPrompt = String(promptText || "");
222
+ const attachments = attachmentOccurrences(rawPrompt);
223
+
224
+ const images = await Promise.all(
225
+ attachments.map(async (attachment) => ({
226
+ type: "image",
227
+ data: await fileToBase64(attachment.file),
228
+ mimeType: attachment.type || "image/png",
229
+ })),
230
+ );
231
+
232
+ return { message: rawPrompt, images };
233
+ }
@@ -0,0 +1,81 @@
1
+ import {
2
+ clearAutocompleteSuggestions,
3
+ detectCdAutocompleteContext,
4
+ detectMentionAutocompleteContext,
5
+ detectSlashCommandAutocompleteContext,
6
+ renderAutocompleteItems,
7
+ } from "./autocomplete.js";
8
+ import { visibleCommandCatalog } from "./command-catalog.js";
9
+ import { el, state } from "./state.js";
10
+ import { sendLocalCommand } from "./transport.js";
11
+
12
+ function activeAutocompleteContext() {
13
+ const value = el.promptInput.value || "";
14
+ const cursor = el.promptInput.selectionStart ?? value.length;
15
+
16
+ const mention = detectMentionAutocompleteContext(value, cursor);
17
+ if (mention) return mention;
18
+
19
+ const cd = detectCdAutocompleteContext(value, cursor);
20
+ if (cd) return cd;
21
+
22
+ const slash = detectSlashCommandAutocompleteContext(value, cursor);
23
+ if (slash) return slash;
24
+
25
+ return null;
26
+ }
27
+
28
+ function requestPathSuggestions(context) {
29
+ if (state.socket?.readyState !== WebSocket.OPEN) {
30
+ renderAutocompleteItems([]);
31
+ return;
32
+ }
33
+
34
+ const requestId = ++state.autocompleteRemoteRequestId;
35
+ sendLocalCommand({
36
+ type: "path-suggestions",
37
+ mode: context.mode,
38
+ query: context.query,
39
+ requestId,
40
+ });
41
+ }
42
+
43
+ function queuePathSuggestions(context) {
44
+ if (state.autocompleteRemoteTimer) {
45
+ clearTimeout(state.autocompleteRemoteTimer);
46
+ }
47
+
48
+ state.autocompleteRemoteTimer = setTimeout(() => {
49
+ state.autocompleteRemoteTimer = null;
50
+ requestPathSuggestions(context);
51
+ }, 90);
52
+ }
53
+
54
+ export function renderCommandSuggestions() {
55
+ const context = activeAutocompleteContext();
56
+ state.autocompleteContext = context;
57
+
58
+ if (!context) {
59
+ clearAutocompleteSuggestions();
60
+ return;
61
+ }
62
+
63
+ if (context.type === "slash-command") {
64
+ const matches = visibleCommandCatalog()
65
+ .filter((command) => command.name.toLowerCase().startsWith(context.query.toLowerCase()))
66
+ .slice(0, 10)
67
+ .map((command) => ({
68
+ kind: command.source === "local" ? (command.insertOnly ? "local-command-insert" : "local-command-run") : "remote-command-insert",
69
+ label: `/${command.name}`,
70
+ badge: command.source || "command",
71
+ description: command.description || "",
72
+ name: command.name,
73
+ }));
74
+
75
+ renderAutocompleteItems(matches);
76
+ return;
77
+ }
78
+
79
+ renderAutocompleteItems([]);
80
+ queuePathSuggestions(context);
81
+ }
@@ -0,0 +1,135 @@
1
+ import { AUTOCOMPLETE_DELIMITERS } from "./constants.js";
2
+ import { el, state } from "./state.js";
3
+ import { escapeAttribute, escapeHtml } from "./formatters.js";
4
+ import { autoResizeTextarea, scheduleComposerLayoutSync } from "./ui.js";
5
+
6
+ export function clearAutocompleteSuggestions() {
7
+ if (state.autocompleteRemoteTimer) {
8
+ clearTimeout(state.autocompleteRemoteTimer);
9
+ state.autocompleteRemoteTimer = null;
10
+ }
11
+ state.autocompleteContext = null;
12
+ state.autocompleteItems = [];
13
+ el.commandStrip.classList.add("hidden");
14
+ el.commandStrip.innerHTML = "";
15
+ scheduleComposerLayoutSync();
16
+ }
17
+
18
+ export function renderAutocompleteItems(items = []) {
19
+ state.autocompleteItems = items;
20
+
21
+ if (!items.length) {
22
+ el.commandStrip.classList.add("hidden");
23
+ el.commandStrip.innerHTML = "";
24
+ scheduleComposerLayoutSync();
25
+ return;
26
+ }
27
+
28
+ el.commandStrip.innerHTML = items.map((item, index) => `
29
+ <button class="command-chip secondary" data-autocomplete-index="${index}" title="${escapeAttribute(item.title || item.description || item.label || "")}">
30
+ <span>${escapeHtml(item.label || "")}</span>
31
+ <span class="source">${escapeHtml(item.badge || "")}</span>
32
+ </button>
33
+ `).join("");
34
+ el.commandStrip.classList.remove("hidden");
35
+ scheduleComposerLayoutSync();
36
+ }
37
+
38
+ function delimiterBeforeIndex(text, index) {
39
+ return index <= 0 || AUTOCOMPLETE_DELIMITERS.has(text[index - 1]);
40
+ }
41
+
42
+ function findTokenBounds(text, start, end) {
43
+ let tokenStart = start;
44
+ let tokenEnd = end;
45
+
46
+ while (tokenStart > 0 && !AUTOCOMPLETE_DELIMITERS.has(text[tokenStart - 1])) {
47
+ tokenStart -= 1;
48
+ }
49
+ while (tokenEnd < text.length && !AUTOCOMPLETE_DELIMITERS.has(text[tokenEnd])) {
50
+ tokenEnd += 1;
51
+ }
52
+
53
+ return { start: tokenStart, end: tokenEnd };
54
+ }
55
+
56
+ export function detectMentionAutocompleteContext(text, cursor) {
57
+ const scanLimit = Math.min(cursor, text.length);
58
+ let tokenStart = scanLimit;
59
+ while (tokenStart > 0 && !AUTOCOMPLETE_DELIMITERS.has(text[tokenStart - 1])) {
60
+ tokenStart -= 1;
61
+ }
62
+
63
+ if (text[tokenStart] !== "@") return null;
64
+ if (!delimiterBeforeIndex(text, tokenStart)) return null;
65
+
66
+ const bounds = findTokenBounds(text, tokenStart, cursor);
67
+ return {
68
+ type: "path",
69
+ mode: "mention",
70
+ query: text.slice(tokenStart + 1, cursor),
71
+ replaceStart: bounds.start,
72
+ replaceEnd: bounds.end,
73
+ };
74
+ }
75
+
76
+ export function detectCdAutocompleteContext(text, cursor) {
77
+ const leadingWhitespace = text.match(/^\s*/)?.[0] || "";
78
+ const trimmed = text.slice(leadingWhitespace.length);
79
+ if (!trimmed.startsWith("/cd")) return null;
80
+
81
+ const afterCommand = trimmed.slice(3);
82
+ if (afterCommand && !/^\s/.test(afterCommand)) return null;
83
+
84
+ const commandStart = leadingWhitespace.length;
85
+ const argsStart = commandStart + 3 + (afterCommand.match(/^\s*/) || [""])[0].length;
86
+ if (cursor < argsStart) return null;
87
+
88
+ return {
89
+ type: "path",
90
+ mode: "cd",
91
+ query: text.slice(argsStart, cursor),
92
+ replaceStart: argsStart,
93
+ replaceEnd: text.length,
94
+ };
95
+ }
96
+
97
+ export function detectSlashCommandAutocompleteContext(text, cursor) {
98
+ const leadingWhitespace = text.match(/^\s*/)?.[0] || "";
99
+ const trimmedBeforeCursor = text.slice(leadingWhitespace.length, cursor);
100
+ if (!trimmedBeforeCursor.startsWith("/")) return null;
101
+ if (/\s/.test(trimmedBeforeCursor.slice(1))) return null;
102
+
103
+ return {
104
+ type: "slash-command",
105
+ query: trimmedBeforeCursor.slice(1),
106
+ };
107
+ }
108
+
109
+ export function replacePromptRange(start, end, nextText) {
110
+ const value = el.promptInput.value;
111
+ el.promptInput.value = `${value.slice(0, start)}${nextText}${value.slice(end)}`;
112
+ const nextCursor = start + nextText.length;
113
+ el.promptInput.focus();
114
+ el.promptInput.setSelectionRange(nextCursor, nextCursor);
115
+ autoResizeTextarea();
116
+ }
117
+
118
+ export function insertTextAtCursor(text) {
119
+ const start = el.promptInput.selectionStart ?? el.promptInput.value.length;
120
+ const end = el.promptInput.selectionEnd ?? start;
121
+ replacePromptRange(start, end, text);
122
+ }
123
+
124
+ export function insertCdCommand() {
125
+ const value = el.promptInput.value;
126
+ if (!value.trim()) {
127
+ el.promptInput.value = "/cd ";
128
+ el.promptInput.focus();
129
+ el.promptInput.setSelectionRange(4, 4);
130
+ autoResizeTextarea();
131
+ return;
132
+ }
133
+
134
+ insertTextAtCursor("/cd ");
135
+ }