@higherdev/cli 0.36.0 → 0.38.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.
- package/dist/attachments.js +112 -20
- package/dist/index.js +20 -8
- package/dist/out.js +1 -1
- package/dist/tui/App.js +78 -25
- package/dist/tui/Help.js +1 -1
- package/dist/tui/TextInput.js +5 -0
- package/dist/tui/parse.js +2 -2
- package/package.json +1 -1
package/dist/attachments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, basename, extname } from "node:path";
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, basename, extname, join } from "node:path";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
4
|
const MIME = {
|
|
5
5
|
".avif": "image/avif", ".bmp": "image/bmp", ".gif": "image/gif", ".heic": "image/heic",
|
|
@@ -8,6 +8,9 @@ const MIME = {
|
|
|
8
8
|
".text": "text/plain", ".md": "text/markdown", ".markdown": "text/markdown",
|
|
9
9
|
".json": "application/json", ".csv": "text/csv",
|
|
10
10
|
};
|
|
11
|
+
export const MAX_ATTACHMENTS_PER_DROP = 20;
|
|
12
|
+
export const PATH_DOES_NOT_EXIST = "That path does not exist";
|
|
13
|
+
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
11
14
|
export function formatAttachmentBytes(size) {
|
|
12
15
|
if (size < 1024)
|
|
13
16
|
return `${size} B`;
|
|
@@ -82,38 +85,127 @@ export function parseAttachArgs(argv) {
|
|
|
82
85
|
}
|
|
83
86
|
rest.push(arg);
|
|
84
87
|
}
|
|
85
|
-
if (rest.length
|
|
86
|
-
throw new Error("usage: hd attach PATH --ticket KEY | --to architect|orchestrator");
|
|
88
|
+
if (!rest.length || Boolean(ticketKey) === Boolean(to)) {
|
|
89
|
+
throw new Error("usage: hd attach PATH [PATH...] --ticket KEY | --to architect|orchestrator");
|
|
87
90
|
}
|
|
88
91
|
if (ticketKey && !/^HD-[1-9][0-9]*$/i.test(ticketKey))
|
|
89
92
|
throw new Error("--ticket must be an HD-N key.");
|
|
90
|
-
return {
|
|
93
|
+
return { paths: rest, ...(ticketKey ? { ticketKey } : {}), ...(to ? { to } : {}) };
|
|
91
94
|
}
|
|
92
95
|
export function parseTuiAttach(text) {
|
|
93
96
|
const words = shellWords(text.trim());
|
|
94
97
|
if (!words?.length)
|
|
95
98
|
return null;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const rest = [];
|
|
100
|
+
let to;
|
|
101
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
102
|
+
if (words[at] !== "--to") {
|
|
103
|
+
rest.push(words[at]);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const role = words[++at];
|
|
107
|
+
if (role !== "architect" && role !== "orchestrator")
|
|
108
|
+
return null;
|
|
109
|
+
to = role;
|
|
110
|
+
}
|
|
111
|
+
const key = rest.at(-1)?.match(/^HD-[1-9][0-9]*$/i)?.[0].toUpperCase();
|
|
112
|
+
const paths = key ? rest.slice(0, -1) : rest;
|
|
113
|
+
if (!paths.length || (key && to))
|
|
114
|
+
return null;
|
|
115
|
+
return { paths, ...(key ? { ticketKey: key } : {}), ...(to ? { to } : {}) };
|
|
116
|
+
}
|
|
117
|
+
export function absolutePathInput(text) {
|
|
118
|
+
const words = shellWords(text.trim());
|
|
119
|
+
return words?.length && words.every(isAbsolute) ? words : [];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Text that a Finder drop produces: one or more absolute paths, plain, quoted, or with
|
|
123
|
+
* escaped spaces. Terminal.app inserts a drop as typed text rather than a bracketed
|
|
124
|
+
* paste, so the input component and the Enter handler both check this before treating
|
|
125
|
+
* a line starting with "/" as a slash command.
|
|
126
|
+
*/
|
|
127
|
+
export function looksLikeDroppedPaths(text) {
|
|
128
|
+
const trimmed = text.trim();
|
|
129
|
+
if (!/^(?:\/|'\/|"\/)/.test(trimmed))
|
|
130
|
+
return false;
|
|
131
|
+
// A slash command is one word without a second "/" (e.g. /architect, /ticket HD-1).
|
|
132
|
+
const firstWord = trimmed.split(/\s+/)[0].replace(/^['"]/, "");
|
|
133
|
+
return firstWord.slice(1).includes("/") || /^['"]/.test(trimmed);
|
|
134
|
+
}
|
|
135
|
+
async function collectFiles(input, files) {
|
|
136
|
+
const info = await stat(input);
|
|
137
|
+
if (info.isFile()) {
|
|
138
|
+
files.push(input);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!info.isDirectory())
|
|
142
|
+
return;
|
|
143
|
+
const entries = await readdir(input, { withFileTypes: true });
|
|
144
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const child = join(input, entry.name);
|
|
147
|
+
if (entry.isDirectory())
|
|
148
|
+
await collectFiles(child, files);
|
|
149
|
+
else if (entry.isFile())
|
|
150
|
+
files.push(child);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function expandAttachmentPaths(inputs, limit = MAX_ATTACHMENTS_PER_DROP) {
|
|
154
|
+
const files = [];
|
|
155
|
+
const missing = [];
|
|
156
|
+
let existingInputs = 0;
|
|
157
|
+
for (const input of inputs) {
|
|
158
|
+
try {
|
|
159
|
+
await collectFiles(input, files);
|
|
160
|
+
existingInputs += 1;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
164
|
+
missing.push(input);
|
|
165
|
+
else
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const unique = [...new Set(files)].sort((left, right) => left.localeCompare(right));
|
|
170
|
+
const skipped = { unsupported: 0, tooLarge: 0, limit: 0 };
|
|
171
|
+
const allowed = [];
|
|
172
|
+
for (const file of unique) {
|
|
173
|
+
if (!MIME[extname(file).toLowerCase()]) {
|
|
174
|
+
skipped.unsupported += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if ((await stat(file)).size > MAX_ATTACHMENT_BYTES) {
|
|
178
|
+
skipped.tooLarge += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (allowed.length >= limit) {
|
|
182
|
+
skipped.limit += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
allowed.push(file);
|
|
186
|
+
}
|
|
187
|
+
return { paths: allowed, missing, existingInputs, skipped };
|
|
188
|
+
}
|
|
189
|
+
export function attachmentSkipNote(expansion) {
|
|
190
|
+
const reasons = [];
|
|
191
|
+
if (expansion.skipped.unsupported)
|
|
192
|
+
reasons.push(`${expansion.skipped.unsupported} unsupported type${expansion.skipped.unsupported === 1 ? "" : "s"}`);
|
|
193
|
+
if (expansion.skipped.tooLarge)
|
|
194
|
+
reasons.push(`${expansion.skipped.tooLarge} over 20 MB`);
|
|
195
|
+
if (expansion.skipped.limit)
|
|
196
|
+
reasons.push(`${expansion.skipped.limit} beyond the 20-file limit`);
|
|
197
|
+
const count = expansion.skipped.unsupported + expansion.skipped.tooLarge + expansion.skipped.limit;
|
|
198
|
+
return count ? `Skipped ${count} file${count === 1 ? "" : "s"}: ${reasons.join(", ")}.` : null;
|
|
99
199
|
}
|
|
100
|
-
export async function detectDroppedPaths(pasted
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
return [];
|
|
104
|
-
if (isAbsolute(text) && await isFile(text).catch(() => false))
|
|
105
|
-
return [text];
|
|
106
|
-
const words = shellWords(text);
|
|
107
|
-
if (!words?.length || words.some((path) => !isAbsolute(path)))
|
|
108
|
-
return [];
|
|
109
|
-
const checks = await Promise.all(words.map((path) => isFile(path).catch(() => false)));
|
|
110
|
-
return checks.every(Boolean) ? words : [];
|
|
200
|
+
export async function detectDroppedPaths(pasted) {
|
|
201
|
+
const paths = absolutePathInput(pasted);
|
|
202
|
+
return paths.length ? (await expandAttachmentPaths(paths)).paths : [];
|
|
111
203
|
}
|
|
112
204
|
export async function uploadAttachment(path, target = {}, config = loadConfig(), fetchImpl = fetch) {
|
|
113
205
|
const info = await stat(path);
|
|
114
206
|
if (!info.isFile())
|
|
115
207
|
throw new Error(`${path} is not a file.`);
|
|
116
|
-
if (info.size >
|
|
208
|
+
if (info.size > MAX_ATTACHMENT_BYTES)
|
|
117
209
|
throw new Error("Attachments must be 20 MB or smaller.");
|
|
118
210
|
const mime = MIME[extname(path).toLowerCase()];
|
|
119
211
|
if (!mime)
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { banner, c, statusChip, table, truncate, usage } from "./out.js";
|
|
|
13
13
|
import { ticketNew } from "./ticket-commands.js";
|
|
14
14
|
import { ticketViewLines } from "./tui/ticket-view.js";
|
|
15
15
|
import { agentAdd, AGENT_ADD_USAGE } from "./agent-commands.js";
|
|
16
|
-
import { attachmentChip, parseAttachArgs, uploadAttachment } from "./attachments.js";
|
|
16
|
+
import { attachmentChip, attachmentSkipNote, expandAttachmentPaths, parseAttachArgs, PATH_DOES_NOT_EXIST, uploadAttachment } from "./attachments.js";
|
|
17
17
|
import { WORKSPACE_USAGE, workspaceGrantRunnerAccess, workspaceNew, workspaceRotateKey, workspaceSet, workspaceUse } from "./workspace-commands.js";
|
|
18
18
|
import { defaultGh, hasWriteRepoPermission, HDX_RUNNER_GH_USER, requireAdminRepo, runnerRepoPermission } from "./workspace-preflight.js";
|
|
19
19
|
function fail(message) {
|
|
@@ -309,14 +309,26 @@ async function cmdMsg(argv) {
|
|
|
309
309
|
});
|
|
310
310
|
console.log(`sent ${message.id}`);
|
|
311
311
|
}
|
|
312
|
-
export async function cmdAttach(argv) {
|
|
312
|
+
export async function cmdAttach(argv, deps = {}) {
|
|
313
313
|
const parsed = parseAttachArgs(argv);
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
314
|
+
const expansion = await expandAttachmentPaths(parsed.paths);
|
|
315
|
+
if (expansion.missing.length)
|
|
316
|
+
throw new Error(PATH_DOES_NOT_EXIST);
|
|
317
|
+
const note = attachmentSkipNote(expansion);
|
|
318
|
+
if (!expansion.paths.length)
|
|
319
|
+
throw new Error(["No supported files found.", note].filter(Boolean).join(" "));
|
|
320
|
+
const config = deps.config ?? loadConfig();
|
|
321
|
+
const attachments = [];
|
|
322
|
+
for (const path of expansion.paths)
|
|
323
|
+
attachments.push(await uploadAttachment(path, {}, config, deps.fetch));
|
|
324
|
+
await (deps.postMessage ?? postMessage)({ body_md: `Attached ${attachments.length} files`,
|
|
325
|
+
to_role: parsed.to ?? "builder", delivery: "queue", ...(parsed.ticketKey ? { ticket_key: parsed.ticketKey } : {}),
|
|
326
|
+
attachment_ids: attachments.map((item) => item.id) }, config);
|
|
327
|
+
for (const attachment of attachments)
|
|
328
|
+
console.log(attachmentChip(attachment));
|
|
329
|
+
console.log(`Attached ${attachments.length} files${parsed.ticketKey ? ` to ${parsed.ticketKey}` : ` to ${parsed.to}`}.`);
|
|
330
|
+
if (note)
|
|
331
|
+
console.log(note);
|
|
320
332
|
}
|
|
321
333
|
async function cmdInbox(argv) {
|
|
322
334
|
const { rest, opts, bools } = flags(argv);
|
package/dist/out.js
CHANGED
|
@@ -76,7 +76,7 @@ export function usage() {
|
|
|
76
76
|
` ${c.blue("hd host runner-install | roll | env")} install CI, roll, or edit host env`,
|
|
77
77
|
` ${c.blue("hd logs KEY [-f]")} run events`,
|
|
78
78
|
` ${c.blue("hd msg KEY TEXT")} message a builder`,
|
|
79
|
-
` ${c.blue("hd attach PATH --ticket KEY | --to ROLE")} attach a
|
|
79
|
+
` ${c.blue("hd attach PATH... --ticket KEY | --to ROLE")} attach files or a folder`,
|
|
80
80
|
` ${c.blue("hd inbox [--all] [--limit N] [--json]")} inbox messages`,
|
|
81
81
|
` ${c.blue("hd decide [ID --answer TEXT]")} decisions`,
|
|
82
82
|
` ${c.blue("hd on | hd off")} workspace switch`,
|
package/dist/tui/App.js
CHANGED
|
@@ -33,7 +33,7 @@ import { editFor, editableKeys, nextValue, seedFor, settingsRows } from "./setti
|
|
|
33
33
|
import { appendLines, runLabels, toStreamLines } from "./stream.js";
|
|
34
34
|
import { UI } from "./theme.js";
|
|
35
35
|
import { WorkspaceLoads } from "./workspace-load.js";
|
|
36
|
-
import { attachmentChip,
|
|
36
|
+
import { absolutePathInput, attachmentChip, attachmentSkipNote, expandAttachmentPaths, looksLikeDroppedPaths, MAX_ATTACHMENTS_PER_DROP, PATH_DOES_NOT_EXIST, uploadAttachment } from "../attachments.js";
|
|
37
37
|
let messageSeq = 0;
|
|
38
38
|
const nextId = () => `m${messageSeq++}`;
|
|
39
39
|
const tuiPrompt = (question) => promptOnStdin(question, true);
|
|
@@ -456,31 +456,43 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
456
456
|
}
|
|
457
457
|
})();
|
|
458
458
|
}, [agentName, config, pendingAttachments, setThread, workspace.id]);
|
|
459
|
+
const uploadExpanded = useCallback(async (inputs, target = {}, prepared, limit = MAX_ATTACHMENTS_PER_DROP) => {
|
|
460
|
+
const expansion = prepared ?? await expandAttachmentPaths(inputs, limit);
|
|
461
|
+
if (expansion.missing.length)
|
|
462
|
+
throw new Error(PATH_DOES_NOT_EXIST);
|
|
463
|
+
const note = attachmentSkipNote(expansion);
|
|
464
|
+
if (!expansion.paths.length)
|
|
465
|
+
throw new Error(["No supported files found.", note].filter(Boolean).join(" "));
|
|
466
|
+
const uploaded = [];
|
|
467
|
+
for (const path of expansion.paths) {
|
|
468
|
+
try {
|
|
469
|
+
uploaded.push(await uploadAttachment(path, { ...target, onProgress: (progress) => setAttachmentUploads((current) => ({ ...current, [path]: progress })) }, config));
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
setAttachmentUploads((current) => {
|
|
473
|
+
const next = { ...current };
|
|
474
|
+
delete next[path];
|
|
475
|
+
return next;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return { uploaded, note };
|
|
480
|
+
}, [config, pendingAttachments.length]);
|
|
459
481
|
const receiveDrop = useCallback((pasted) => {
|
|
460
|
-
if (
|
|
482
|
+
if (!looksLikeDroppedPaths(pasted))
|
|
461
483
|
return false;
|
|
462
484
|
void (async () => {
|
|
463
|
-
const
|
|
464
|
-
if (!
|
|
485
|
+
const inputs = absolutePathInput(pasted);
|
|
486
|
+
if (!inputs.length) {
|
|
465
487
|
setDraft((current) => current + pasted.replace(/[\r\n]+/g, " "));
|
|
466
488
|
return;
|
|
467
489
|
}
|
|
468
490
|
setBusy(true);
|
|
469
491
|
try {
|
|
470
|
-
const uploaded =
|
|
471
|
-
for (const path of paths) {
|
|
472
|
-
try {
|
|
473
|
-
uploaded.push(await uploadAttachment(path, { onProgress: (progress) => setAttachmentUploads((current) => ({ ...current, [path]: progress })) }, config));
|
|
474
|
-
}
|
|
475
|
-
finally {
|
|
476
|
-
setAttachmentUploads((current) => {
|
|
477
|
-
const next = { ...current };
|
|
478
|
-
delete next[path];
|
|
479
|
-
return next;
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
}
|
|
492
|
+
const { uploaded, note } = await uploadExpanded(inputs, {}, undefined, Math.max(0, MAX_ATTACHMENTS_PER_DROP - pendingAttachments.length));
|
|
483
493
|
setPendingAttachments((current) => [...current, ...uploaded]);
|
|
494
|
+
if (note)
|
|
495
|
+
setNotice(note);
|
|
484
496
|
}
|
|
485
497
|
catch (error) {
|
|
486
498
|
setNotice(error instanceof Error ? error.message : String(error));
|
|
@@ -490,7 +502,7 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
490
502
|
}
|
|
491
503
|
})();
|
|
492
504
|
return true;
|
|
493
|
-
}, [
|
|
505
|
+
}, [uploadExpanded]);
|
|
494
506
|
const openTicket = useCallback(async (key) => {
|
|
495
507
|
setTicketKey(key);
|
|
496
508
|
setView("ticket");
|
|
@@ -520,6 +532,28 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
520
532
|
}, [view, ticketKey, visibleStory?.latest_headline, visibleStory?.timeline, config]);
|
|
521
533
|
const run = useCallback(async (raw) => {
|
|
522
534
|
const text = raw.trim();
|
|
535
|
+
// An existing absolute path always wins over slash-command parsing in every view.
|
|
536
|
+
const absolutePaths = absolutePathInput(text);
|
|
537
|
+
if (absolutePaths.length) {
|
|
538
|
+
const expansion = await expandAttachmentPaths(absolutePaths, Math.max(0, 20 - pendingAttachments.length));
|
|
539
|
+
if (expansion.existingInputs) {
|
|
540
|
+
setDraft("");
|
|
541
|
+
setBusy(true);
|
|
542
|
+
try {
|
|
543
|
+
const { uploaded, note } = await uploadExpanded(absolutePaths, {}, expansion);
|
|
544
|
+
setPendingAttachments((current) => [...current, ...uploaded]);
|
|
545
|
+
if (note)
|
|
546
|
+
setNotice(note);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
setNotice(error instanceof Error ? error.message : String(error));
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
setBusy(false);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
523
557
|
if (view === "settings") {
|
|
524
558
|
const open = editingRef.current;
|
|
525
559
|
if (open) {
|
|
@@ -582,6 +616,10 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
582
616
|
setDraft("");
|
|
583
617
|
setNotice(null);
|
|
584
618
|
const action = parseLine(text);
|
|
619
|
+
if (absolutePaths.length && action.kind === "unknown") {
|
|
620
|
+
setNotice(PATH_DOES_NOT_EXIST);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
585
623
|
if (action.kind === "say") {
|
|
586
624
|
if (mode !== "browse")
|
|
587
625
|
askAgent(mode, text);
|
|
@@ -880,12 +918,26 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
880
918
|
case "attach":
|
|
881
919
|
setBusy(true);
|
|
882
920
|
try {
|
|
883
|
-
const key = action.key ?? ticketKey;
|
|
884
|
-
if (!key)
|
|
885
|
-
throw new Error("
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
await
|
|
921
|
+
const key = action.key ?? (view === "ticket" ? ticketKey : null);
|
|
922
|
+
if (!key && !action.to && view !== "chat") {
|
|
923
|
+
throw new Error("Choose a target: open a ticket, add HD-N, or use --to architect|orchestrator.");
|
|
924
|
+
}
|
|
925
|
+
const queuesForChat = !key && !action.to;
|
|
926
|
+
const { uploaded, note } = await uploadExpanded(action.paths, key ? { ticketKey: key } : {}, undefined, queuesForChat ? Math.max(0, MAX_ATTACHMENTS_PER_DROP - pendingAttachments.length) : MAX_ATTACHMENTS_PER_DROP);
|
|
927
|
+
if (action.to) {
|
|
928
|
+
await postAgentMessage(action.to, `Attached ${uploaded.length} files`, config, uploaded.map((attachment) => attachment.id));
|
|
929
|
+
say("system", `Attached ${uploaded.length} files to ${action.to}.`);
|
|
930
|
+
}
|
|
931
|
+
else if (key) {
|
|
932
|
+
say("system", `Attached ${uploaded.length} files to ${key}.`);
|
|
933
|
+
await refresh();
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
setPendingAttachments((current) => [...current, ...uploaded]);
|
|
937
|
+
say("system", `Attached ${uploaded.length} files to your next ${mode} message.`);
|
|
938
|
+
}
|
|
939
|
+
if (note)
|
|
940
|
+
setNotice(note);
|
|
889
941
|
}
|
|
890
942
|
catch (error) {
|
|
891
943
|
setNotice(error instanceof Error ? error.message : String(error));
|
|
@@ -950,7 +1002,8 @@ export function App({ initial, availableUpdate: initialUpdate = null, updateDeps
|
|
|
950
1002
|
}
|
|
951
1003
|
}, [view, settings, applyEdit, board, browsing, mode, say, askAgent, openAgentChat, order, settingsOrder,
|
|
952
1004
|
changeWorkspace, config, refresh, suspendTerminal, exit, inbox, inboxFocus, openTicket, answering,
|
|
953
|
-
selectedDecisionId, submitDecision, ticketKey, ticketCollapsed, ticketOffset, width, pendingAttachments
|
|
1005
|
+
selectedDecisionId, submitDecision, ticketKey, ticketCollapsed, ticketOffset, width, pendingAttachments,
|
|
1006
|
+
uploadExpanded]);
|
|
954
1007
|
useInput((input, key) => {
|
|
955
1008
|
if (key.ctrl && input === "c") {
|
|
956
1009
|
exit();
|
package/dist/tui/Help.js
CHANGED
|
@@ -13,7 +13,7 @@ export const COMMANDS = [
|
|
|
13
13
|
{ name: "/cancel", args: "HD-12 | RUN-ID", help: "cancel a ticket or run" },
|
|
14
14
|
{ name: "/merge", args: "HD-12", help: "approve a reviewed PR over the reviewer's objections" },
|
|
15
15
|
{ name: "/msg", args: "HD-12 TEXT", help: "message a ticket's builder" },
|
|
16
|
-
{ name: "/attach", args: "PATH [HD-12]", help: "attach
|
|
16
|
+
{ name: "/attach", args: "PATH... [HD-12 | --to ROLE]", help: "attach files, or queue them for this chat" },
|
|
17
17
|
{ name: "/logs", args: "[raw] [HD-12]", help: "filter activity; raw reveals event JSON" },
|
|
18
18
|
{ name: "/epic", args: "new PATH [--draft] | approve ID | rm ID", help: "create, approve, or remove an epic" },
|
|
19
19
|
{ name: "/epics", help: "list epics and ticket progress" },
|
package/dist/tui/TextInput.js
CHANGED
|
@@ -73,6 +73,11 @@ export default function TextInput({ value, onChange, onSubmit, onCancel, onUp, o
|
|
|
73
73
|
// most of them. Ink reports that as ordinary input with key.return
|
|
74
74
|
// false, so without this the Enter is filtered out with the other
|
|
75
75
|
// control bytes and the line just sits at the prompt unsent.
|
|
76
|
+
// A Finder drop in Terminal.app arrives as one multi-character chunk of
|
|
77
|
+
// typed text, not a bracketed paste, so it is offered to the paste
|
|
78
|
+
// handler first.
|
|
79
|
+
if (input.length > 1 && onPasteText?.(input))
|
|
80
|
+
return;
|
|
76
81
|
if (input.length > 1 && /[\r\n]/.test(input)) {
|
|
77
82
|
const parts = input.split(/\r\n|\r|\n/);
|
|
78
83
|
const submits = parts.length === 2 && parts[1] === "";
|
package/dist/tui/parse.js
CHANGED
|
@@ -105,8 +105,8 @@ export function parseLine(raw) {
|
|
|
105
105
|
: { kind: "unknown", command: "msg needs KEY TEXT" };
|
|
106
106
|
case "attach": {
|
|
107
107
|
const parsed = parseTuiAttach(argument);
|
|
108
|
-
return parsed ? { kind: "attach",
|
|
109
|
-
: { kind: "unknown", command: "attach needs PATH and
|
|
108
|
+
return parsed ? { kind: "attach", paths: parsed.paths, key: parsed.ticketKey ?? null, to: parsed.to ?? null }
|
|
109
|
+
: { kind: "unknown", command: "attach needs PATH [PATH...] and HD-N or --to architect|orchestrator" };
|
|
110
110
|
}
|
|
111
111
|
case "logs":
|
|
112
112
|
if (rest[0]?.toLowerCase() === "raw" && rest.length <= 2) {
|