@buzzni/saycode-cli 0.1.1 → 0.1.2
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/README.md +21 -6
- package/index.mjs +452 -115
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,15 +103,22 @@ saycode chat -s <session> # 기존 세션 이어서 대화 (직전 20개
|
|
|
103
103
|
|
|
104
104
|
## 대화에서 나가기
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
채팅 하단 상태가 현재 입력 가능 여부를 그대로 나타냅니다.
|
|
107
107
|
|
|
108
108
|
```
|
|
109
|
-
|
|
109
|
+
● Ready — you can type
|
|
110
|
+
› 다음 작업을 진행해줘
|
|
111
|
+
● Agent is responding — input locked · Ctrl+C for options
|
|
110
112
|
⏺ Bash(npm test)
|
|
111
113
|
│ running tests...
|
|
112
114
|
^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response
|
|
113
115
|
```
|
|
114
116
|
|
|
117
|
+
`Agent is responding` 동안 stdin은 raw/no-echo로 잠겨 일반 문자와 Enter가 화면에 나타나거나 다음
|
|
118
|
+
프롬프트에 쌓이지 않습니다. 권한 요청 때만 `◆ Action required — permission response only`로 바뀌고
|
|
119
|
+
해당 선택 입력을 받습니다. 에이전트 턴이 끝나 `Ready`가 표시된 뒤에만 일반 프롬프트가 열립니다.
|
|
120
|
+
|
|
121
|
+
에이전트가 작업 중일 때 **`Ctrl+C`를 누르면 선택지가 뜹니다.**
|
|
115
122
|
한 글자만 누르면 됩니다 (Enter 불필요). `Ctrl+C`를 한 번 더 누르면 `d`와 같습니다 —
|
|
116
123
|
반복해서 누르는 건 "빨리 나가자"는 뜻이고, 나가는 건 되돌릴 수 있지만 작업을 죽이는 건
|
|
117
124
|
되돌릴 수 없기 때문입니다.
|
|
@@ -128,9 +135,17 @@ saycode chat -s <session> # 기존 세션 이어서 대화 (직전 20개
|
|
|
128
135
|
**어느 경우에도 원격 세션은 죽지 않습니다.** 데스크탑이나 다른 터미널에서 하던 작업을
|
|
129
136
|
CLI를 닫았다는 이유로 파괴하지 않습니다.
|
|
130
137
|
|
|
131
|
-
응답이 오래 걸릴 때
|
|
138
|
+
응답이 오래 걸릴 때 `Ctrl+C` → `d`로 빠져나와 `saycode watch`로 진행만 지켜보다가,
|
|
132
139
|
끝나면 `saycode chat -s <id>`로 돌아오는 흐름을 의도했습니다.
|
|
133
140
|
|
|
141
|
+
## 답변 표시
|
|
142
|
+
|
|
143
|
+
에이전트 답변의 제목·목록·인용·굵게·기울임·취소선·인라인 코드·링크·fenced code block을
|
|
144
|
+
터미널 표현으로 바꾸고, 현재 터미널 폭에서 좌우 여백을 뺀 최대 100열로 줄바꿈합니다. 한글과
|
|
145
|
+
emoji의 표시 폭도 반영합니다. 링크는 파이프 출력에서도 목적지를 잃지 않도록 URL을 함께 남깁니다.
|
|
146
|
+
긴 도구 stdout은 첫 3줄까지만 표시하고 종료 시 생략한 줄 수와 마지막 줄을 요약하며, stderr는
|
|
147
|
+
숨기지 않습니다. `NO_COLOR`이거나 파이프로 출력해도 `◆`, `•`, `│`, `┌─` 같은 구조 표시는 남습니다.
|
|
148
|
+
|
|
134
149
|
## 권한 승인
|
|
135
150
|
|
|
136
151
|
에이전트가 권한을 요청하면 인라인으로 물어봅니다.
|
|
@@ -154,11 +169,11 @@ GitHub Actions 가 태그로 발행합니다 (`.github/workflows/publish-saycode
|
|
|
154
169
|
|
|
155
170
|
```bash
|
|
156
171
|
# 1. 버전 올리기 — 데스크탑 앱 버전과 무관합니다
|
|
157
|
-
vi cli/manifest.json # "version": "0.1.
|
|
172
|
+
vi cli/manifest.json # "version": "0.1.2"
|
|
158
173
|
|
|
159
174
|
# 2. 커밋하고 태그를 밀면 끝
|
|
160
|
-
git tag saycode-cli-v0.1.
|
|
161
|
-
git push origin saycode-cli-v0.1.
|
|
175
|
+
git tag saycode-cli-v0.1.2
|
|
176
|
+
git push origin saycode-cli-v0.1.2
|
|
162
177
|
```
|
|
163
178
|
|
|
164
179
|
워크플로는 발행 전에 이 순서로 막습니다:
|
package/index.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function _mergeNamespaces(n, m) {
|
|
|
37
37
|
}
|
|
38
38
|
return Object.freeze(Object.defineProperty(n, Symbol.toStringTag, { value: "Module" }));
|
|
39
39
|
}
|
|
40
|
-
const version = "0.1.
|
|
40
|
+
const version = "0.1.2";
|
|
41
41
|
const manifest = {
|
|
42
42
|
version
|
|
43
43
|
};
|
|
@@ -23565,7 +23565,7 @@ class CliRest {
|
|
|
23565
23565
|
/** Newest-last page of a session transcript, used for `attach --since`. */
|
|
23566
23566
|
async recentMessages(sessionId, limit) {
|
|
23567
23567
|
const data = await this.get(
|
|
23568
|
-
`/v3/sessions/${encodeURIComponent(sessionId)}/messages
|
|
23568
|
+
`/v3/sessions/${encodeURIComponent(sessionId)}/messages?${new URLSearchParams({ limit: String(limit), before_seq: "2147483647" })}`
|
|
23569
23569
|
);
|
|
23570
23570
|
return data.messages ?? [];
|
|
23571
23571
|
}
|
|
@@ -23625,17 +23625,34 @@ const RESET = "\x1B[0m";
|
|
|
23625
23625
|
const STYLE_CODES = {
|
|
23626
23626
|
user: "\x1B[1;36m",
|
|
23627
23627
|
thinking: "\x1B[2;37m",
|
|
23628
|
-
tool: "\x1B[33m",
|
|
23628
|
+
tool: "\x1B[2;33m",
|
|
23629
23629
|
progress: "\x1B[2;37m",
|
|
23630
23630
|
status: "\x1B[32m",
|
|
23631
|
-
error: "\x1B[31m"
|
|
23631
|
+
error: "\x1B[31m",
|
|
23632
|
+
heading: "\x1B[1;36m",
|
|
23633
|
+
quote: "\x1B[2;37m",
|
|
23634
|
+
code: "\x1B[36m"
|
|
23632
23635
|
// `text` is intentionally absent: agent prose is the main content and reads
|
|
23633
23636
|
// best in the terminal's own foreground colour.
|
|
23634
23637
|
};
|
|
23638
|
+
const INLINE_STYLE_CODES = {
|
|
23639
|
+
strong: "\x1B[1m",
|
|
23640
|
+
emphasis: "\x1B[3m",
|
|
23641
|
+
inlineCode: "\x1B[36m",
|
|
23642
|
+
strike: "\x1B[9m",
|
|
23643
|
+
link: "\x1B[4;36m"
|
|
23644
|
+
};
|
|
23645
|
+
function formatSpans(spans, baseCode) {
|
|
23646
|
+
return spans.map((span) => {
|
|
23647
|
+
if (!span.style) return span.text;
|
|
23648
|
+
return `${INLINE_STYLE_CODES[span.style]}${span.text}${baseCode ?? RESET}`;
|
|
23649
|
+
}).join("");
|
|
23650
|
+
}
|
|
23635
23651
|
function formatLine(line, colour) {
|
|
23636
23652
|
if (line.style === "divider") return "";
|
|
23637
23653
|
const code = colour ? STYLE_CODES[line.style] : void 0;
|
|
23638
|
-
|
|
23654
|
+
const text2 = colour && line.spans ? formatSpans(line.spans, code) : line.text;
|
|
23655
|
+
return code ? `${code}${text2}${RESET}` : text2;
|
|
23639
23656
|
}
|
|
23640
23657
|
function colourEnabled(env, isTty) {
|
|
23641
23658
|
if (env.NO_COLOR !== void 0) return false;
|
|
@@ -23854,7 +23871,171 @@ function runLogout(out) {
|
|
|
23854
23871
|
}
|
|
23855
23872
|
out.raw("No stored Saycode credentials. Happy credentials (~/.happy*/access.key) are untouched.");
|
|
23856
23873
|
}
|
|
23874
|
+
function isCombining(codePoint, character) {
|
|
23875
|
+
return codePoint === 8205 || new RegExp("\\p{Mark}", "u").test(character);
|
|
23876
|
+
}
|
|
23877
|
+
function isWide(codePoint) {
|
|
23878
|
+
return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 127744 && codePoint <= 129791 || codePoint >= 131072 && codePoint <= 262141);
|
|
23879
|
+
}
|
|
23880
|
+
function displayWidth(text2) {
|
|
23881
|
+
let width = 0;
|
|
23882
|
+
for (const character of text2) {
|
|
23883
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
23884
|
+
if (codePoint < 32 || codePoint >= 127 && codePoint < 160 || isCombining(codePoint, character)) continue;
|
|
23885
|
+
width += isWide(codePoint) ? 2 : 1;
|
|
23886
|
+
}
|
|
23887
|
+
return width;
|
|
23888
|
+
}
|
|
23889
|
+
function styledToken(source, marker, style) {
|
|
23890
|
+
if (!source.startsWith(marker)) return null;
|
|
23891
|
+
const end = source.indexOf(marker, marker.length);
|
|
23892
|
+
if (end < marker.length + 1) return null;
|
|
23893
|
+
const text2 = source.slice(marker.length, end);
|
|
23894
|
+
if (text2.trim() === "") return null;
|
|
23895
|
+
return { length: end + marker.length, spans: [{ style, text: text2 }] };
|
|
23896
|
+
}
|
|
23897
|
+
function tokenAt(source) {
|
|
23898
|
+
const escaped = source.match(/^\\([\\`*_[\]~()])/);
|
|
23899
|
+
if (escaped) return { length: escaped[0].length, spans: [{ text: escaped[1] ?? "" }] };
|
|
23900
|
+
const code = source.match(/^`([^`\n]+)`/);
|
|
23901
|
+
if (code) return { length: code[0].length, spans: [{ style: "inlineCode", text: code[1] ?? "" }] };
|
|
23902
|
+
const link = source.match(/^\[([^\]\n]+)]\((https?:\/\/[^\s)\n]+)\)/);
|
|
23903
|
+
if (link) {
|
|
23904
|
+
const text2 = link[1] ?? "";
|
|
23905
|
+
const href = link[2] ?? "";
|
|
23906
|
+
return {
|
|
23907
|
+
length: link[0].length,
|
|
23908
|
+
spans: [{ href, style: "link", text: text2 }, { text: ` (${href})` }]
|
|
23909
|
+
};
|
|
23910
|
+
}
|
|
23911
|
+
return styledToken(source, "**", "strong") ?? styledToken(source, "~~", "strike") ?? (source.startsWith("**") ? null : styledToken(source, "*", "emphasis"));
|
|
23912
|
+
}
|
|
23913
|
+
function appendSpan(spans, span) {
|
|
23914
|
+
if (span.text === "") return;
|
|
23915
|
+
const previous = spans.at(-1);
|
|
23916
|
+
if (previous && previous.style === span.style && previous.href === span.href) {
|
|
23917
|
+
previous.text += span.text;
|
|
23918
|
+
return;
|
|
23919
|
+
}
|
|
23920
|
+
spans.push({ ...span });
|
|
23921
|
+
}
|
|
23922
|
+
function parseInlineMarkdown(source) {
|
|
23923
|
+
const spans = [];
|
|
23924
|
+
let offset = 0;
|
|
23925
|
+
while (offset < source.length) {
|
|
23926
|
+
const token = tokenAt(source.slice(offset));
|
|
23927
|
+
if (token) {
|
|
23928
|
+
for (const span of token.spans) appendSpan(spans, span);
|
|
23929
|
+
offset += token.length;
|
|
23930
|
+
continue;
|
|
23931
|
+
}
|
|
23932
|
+
const character = String.fromCodePoint(source.codePointAt(offset) ?? 0);
|
|
23933
|
+
appendSpan(spans, { text: character });
|
|
23934
|
+
offset += character.length;
|
|
23935
|
+
}
|
|
23936
|
+
return spans;
|
|
23937
|
+
}
|
|
23938
|
+
function finishLine(spans) {
|
|
23939
|
+
while (spans.at(-1)?.text.endsWith(" ")) {
|
|
23940
|
+
const last = spans.at(-1);
|
|
23941
|
+
if (!last) break;
|
|
23942
|
+
last.text = last.text.trimEnd();
|
|
23943
|
+
if (last.text === "") spans.pop();
|
|
23944
|
+
}
|
|
23945
|
+
return { text: spans.map((span) => span.text).join(""), spans };
|
|
23946
|
+
}
|
|
23947
|
+
function renderInlineMarkdown(source, width) {
|
|
23948
|
+
const parsed = parseInlineMarkdown(source);
|
|
23949
|
+
const lines = [];
|
|
23950
|
+
let current = [];
|
|
23951
|
+
let currentWidth = 0;
|
|
23952
|
+
for (const span of parsed) {
|
|
23953
|
+
for (const character of span.text) {
|
|
23954
|
+
const characterWidth = displayWidth(character);
|
|
23955
|
+
if (current.length > 0 && currentWidth + characterWidth > width) {
|
|
23956
|
+
lines.push(finishLine(current));
|
|
23957
|
+
current = [];
|
|
23958
|
+
currentWidth = 0;
|
|
23959
|
+
}
|
|
23960
|
+
if (current.length === 0 && character === " ") continue;
|
|
23961
|
+
appendSpan(current, { ...span, text: character });
|
|
23962
|
+
currentWidth += characterWidth;
|
|
23963
|
+
}
|
|
23964
|
+
}
|
|
23965
|
+
if (current.length > 0 || lines.length === 0) lines.push(finishLine(current));
|
|
23966
|
+
return lines;
|
|
23967
|
+
}
|
|
23968
|
+
function withPrefix(style, prefix, line) {
|
|
23969
|
+
if (!line.spans.some((span) => span.style)) return { style, text: `${prefix}${line.text}` };
|
|
23970
|
+
return { style, text: `${prefix}${line.text}`, spans: [...prefix ? [{ text: prefix }] : [], ...line.spans] };
|
|
23971
|
+
}
|
|
23972
|
+
class TerminalMarkdownRenderer {
|
|
23973
|
+
constructor(width) {
|
|
23974
|
+
this.width = width;
|
|
23975
|
+
}
|
|
23976
|
+
pending = "";
|
|
23977
|
+
inFence = false;
|
|
23978
|
+
get hasPending() {
|
|
23979
|
+
return this.pending !== "";
|
|
23980
|
+
}
|
|
23981
|
+
push(chunk) {
|
|
23982
|
+
const normalized = `${this.pending}${chunk}`.replace(/\r\n?/g, "\n");
|
|
23983
|
+
const sourceLines = normalized.split("\n");
|
|
23984
|
+
this.pending = sourceLines.pop() ?? "";
|
|
23985
|
+
return sourceLines.flatMap((line) => this.renderLine(line));
|
|
23986
|
+
}
|
|
23987
|
+
flush() {
|
|
23988
|
+
const pending = this.pending;
|
|
23989
|
+
this.pending = "";
|
|
23990
|
+
const lines = pending === "" ? [] : this.renderLine(pending);
|
|
23991
|
+
if (!this.inFence) return lines;
|
|
23992
|
+
this.inFence = false;
|
|
23993
|
+
return [...lines, { style: "code", text: "└─" }];
|
|
23994
|
+
}
|
|
23995
|
+
prefixed(style, prefix, content, continuation = " ") {
|
|
23996
|
+
const firstWidth = Math.max(1, this.width - displayWidth(prefix));
|
|
23997
|
+
const wrapped = renderInlineMarkdown(content, firstWidth);
|
|
23998
|
+
const lines = [withPrefix(style, prefix, wrapped[0] ?? { spans: [], text: "" })];
|
|
23999
|
+
for (const part of wrapped.slice(1)) {
|
|
24000
|
+
lines.push(withPrefix(style, continuation, part));
|
|
24001
|
+
}
|
|
24002
|
+
return lines;
|
|
24003
|
+
}
|
|
24004
|
+
renderLine(source) {
|
|
24005
|
+
const fence = source.match(/^\s*```\s*([^`]*)$/);
|
|
24006
|
+
if (fence) {
|
|
24007
|
+
if (this.inFence) {
|
|
24008
|
+
this.inFence = false;
|
|
24009
|
+
return [{ style: "code", text: "└─" }];
|
|
24010
|
+
}
|
|
24011
|
+
this.inFence = true;
|
|
24012
|
+
const language = fence[1]?.trim();
|
|
24013
|
+
return [{ style: "code", text: language ? `┌─ ${language}` : "┌─" }];
|
|
24014
|
+
}
|
|
24015
|
+
if (this.inFence) return this.prefixed("code", "│ ", source, "│ ");
|
|
24016
|
+
if (source.trim() === "") return [{ style: "divider", text: "" }];
|
|
24017
|
+
const heading = source.match(/^\s{0,3}#{1,6}\s+(.+)$/);
|
|
24018
|
+
if (heading) return this.prefixed("heading", "◆ ", heading[1] ?? "");
|
|
24019
|
+
const quote = source.match(/^\s*>\s?(.*)$/);
|
|
24020
|
+
if (quote) return this.prefixed("quote", "│ ", quote[1] ?? "", "│ ");
|
|
24021
|
+
const list = source.match(/^(\s*)([-+*]|\d+\.)\s+(.+)$/);
|
|
24022
|
+
if (list) {
|
|
24023
|
+
const indent = list[1] ?? "";
|
|
24024
|
+
const marker = /^\d/.test(list[2] ?? "") ? list[2] : "•";
|
|
24025
|
+
const prefix = `${indent}${marker} `;
|
|
24026
|
+
return this.prefixed("text", prefix, list[3] ?? "", " ".repeat(displayWidth(prefix)));
|
|
24027
|
+
}
|
|
24028
|
+
return renderInlineMarkdown(source, this.width).map((line) => withPrefix("text", "", line));
|
|
24029
|
+
}
|
|
24030
|
+
}
|
|
24031
|
+
function createTerminalMarkdownRenderer(width = 100) {
|
|
24032
|
+
return new TerminalMarkdownRenderer(Math.max(8, width));
|
|
24033
|
+
}
|
|
24034
|
+
function terminalContentWidth(columns) {
|
|
24035
|
+
return Math.max(20, Math.min(100, (columns ?? 104) - 4));
|
|
24036
|
+
}
|
|
23857
24037
|
const TOOL_SUMMARY_MAX_LENGTH = 80;
|
|
24038
|
+
const TOOL_PROGRESS_VISIBLE_LINES = 3;
|
|
23858
24039
|
const TOOL_SUMMARY_KEYS = ["command", "file_path", "path", "pattern", "query", "url", "prompt"];
|
|
23859
24040
|
function truncate(value2, max) {
|
|
23860
24041
|
if (value2.length <= max) return value2;
|
|
@@ -23877,7 +24058,7 @@ function textLines(text2, style) {
|
|
|
23877
24058
|
if (text2.trim() === "") return [];
|
|
23878
24059
|
return text2.split("\n").map((line) => ({ style, text: line }));
|
|
23879
24060
|
}
|
|
23880
|
-
function renderSessionEvent(ev, envelopeRole) {
|
|
24061
|
+
function renderSessionEvent(ev, envelopeRole, progressByCall) {
|
|
23881
24062
|
switch (ev.t) {
|
|
23882
24063
|
case "text":
|
|
23883
24064
|
if (envelopeRole === "user") return textLines(ev.text, "user").map((line) => ({ ...line, text: `› ${line.text}` }));
|
|
@@ -23885,16 +24066,35 @@ function renderSessionEvent(ev, envelopeRole) {
|
|
|
23885
24066
|
case "service":
|
|
23886
24067
|
return textLines(ev.text, "status");
|
|
23887
24068
|
case "tool-call-start": {
|
|
24069
|
+
progressByCall.set(ev.call, { shown: 0, hidden: 0, lastHidden: null });
|
|
23888
24070
|
const summary = summarizeToolArgs(ev.args) ?? (ev.description?.trim() || null);
|
|
23889
24071
|
return [{ style: "tool", text: summary ? `⏺ ${ev.name}(${summary})` : `⏺ ${ev.name}` }];
|
|
23890
24072
|
}
|
|
23891
|
-
case "tool-call-progress":
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
23895
|
-
|
|
23896
|
-
|
|
23897
|
-
|
|
24073
|
+
case "tool-call-progress": {
|
|
24074
|
+
const state = progressByCall.get(ev.call) ?? { shown: 0, hidden: 0, lastHidden: null };
|
|
24075
|
+
progressByCall.set(ev.call, state);
|
|
24076
|
+
const lines = [];
|
|
24077
|
+
for (const line of ev.lines) {
|
|
24078
|
+
if (ev.stream === "stderr") lines.push({ style: "error", text: ` │ ${line}` });
|
|
24079
|
+
else if (state.shown < TOOL_PROGRESS_VISIBLE_LINES) {
|
|
24080
|
+
state.shown += 1;
|
|
24081
|
+
lines.push({ style: "progress", text: ` │ ${line}` });
|
|
24082
|
+
} else {
|
|
24083
|
+
state.hidden += 1;
|
|
24084
|
+
state.lastHidden = line;
|
|
24085
|
+
}
|
|
24086
|
+
}
|
|
24087
|
+
return lines;
|
|
24088
|
+
}
|
|
24089
|
+
case "tool-call-end": {
|
|
24090
|
+
const state = progressByCall.get(ev.call);
|
|
24091
|
+
progressByCall.delete(ev.call);
|
|
24092
|
+
if (!state?.hidden || !state.lastHidden) return [];
|
|
24093
|
+
return [{
|
|
24094
|
+
style: "progress",
|
|
24095
|
+
text: ` └ ${state.hidden} more lines hidden · last: ${truncate(state.lastHidden, TOOL_SUMMARY_MAX_LENGTH)}`
|
|
24096
|
+
}];
|
|
24097
|
+
}
|
|
23898
24098
|
case "file":
|
|
23899
24099
|
return [{ style: "status", text: `📎 ${ev.name} (${formatBytes(ev.size)})` }];
|
|
23900
24100
|
case "turn-start":
|
|
@@ -23907,14 +24107,34 @@ function renderSessionEvent(ev, envelopeRole) {
|
|
|
23907
24107
|
return [];
|
|
23908
24108
|
}
|
|
23909
24109
|
}
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
24110
|
+
class SessionEventRenderer {
|
|
24111
|
+
markdown;
|
|
24112
|
+
progressByCall = /* @__PURE__ */ new Map();
|
|
24113
|
+
constructor(width) {
|
|
24114
|
+
this.markdown = createTerminalMarkdownRenderer(width);
|
|
24115
|
+
}
|
|
24116
|
+
flush() {
|
|
24117
|
+
return this.markdown.flush();
|
|
23913
24118
|
}
|
|
23914
|
-
|
|
23915
|
-
|
|
24119
|
+
render(content) {
|
|
24120
|
+
if (content.role === "user") {
|
|
24121
|
+
return [...this.flush(), ...textLines(content.content.text, "user").map((line) => ({ ...line, text: `› ${line.text}` }))];
|
|
24122
|
+
}
|
|
24123
|
+
if (content.role !== "session") return [];
|
|
24124
|
+
const { ev, role } = content.content;
|
|
24125
|
+
if (ev.t === "text" && role === "agent" && !ev.thinking) {
|
|
24126
|
+
if (!this.markdown.hasPending && ev.text.trim() === "") return [];
|
|
24127
|
+
return this.markdown.push(ev.text);
|
|
24128
|
+
}
|
|
24129
|
+
const prefix = this.flush();
|
|
24130
|
+
if (ev.t === "turn-start") this.progressByCall.clear();
|
|
24131
|
+
const rendered = renderSessionEvent(ev, role, this.progressByCall);
|
|
24132
|
+
if (ev.t === "turn-end") this.progressByCall.clear();
|
|
24133
|
+
return [...prefix, ...rendered];
|
|
23916
24134
|
}
|
|
23917
|
-
|
|
24135
|
+
}
|
|
24136
|
+
function createSessionEventRenderer(width = 100) {
|
|
24137
|
+
return new SessionEventRenderer(width);
|
|
23918
24138
|
}
|
|
23919
24139
|
function record(value2) {
|
|
23920
24140
|
return value2 && typeof value2 === "object" && !Array.isArray(value2) ? value2 : null;
|
|
@@ -23952,10 +24172,10 @@ function decodeContent(transport, ciphertext) {
|
|
|
23952
24172
|
return null;
|
|
23953
24173
|
}
|
|
23954
24174
|
}
|
|
23955
|
-
function printContent(context, content) {
|
|
23956
|
-
context.out.lines(
|
|
24175
|
+
function printContent(context, content, renderer = createSessionEventRenderer()) {
|
|
24176
|
+
context.out.lines(renderer.render(content));
|
|
23957
24177
|
}
|
|
23958
|
-
async function prerollHistory(context, transport, sessionId, since) {
|
|
24178
|
+
async function prerollHistory(context, transport, sessionId, since, onContent, renderer = createSessionEventRenderer()) {
|
|
23959
24179
|
if (since <= 0) return 0;
|
|
23960
24180
|
const messages = await context.rest.recentMessages(sessionId, since);
|
|
23961
24181
|
let highestSeq = 0;
|
|
@@ -23964,28 +24184,31 @@ async function prerollHistory(context, transport, sessionId, since) {
|
|
|
23964
24184
|
const ciphertext = unwrapEncryptedField(message.content);
|
|
23965
24185
|
if (!ciphertext) continue;
|
|
23966
24186
|
const content = decodeContent(transport, ciphertext);
|
|
23967
|
-
if (content)
|
|
24187
|
+
if (!content) continue;
|
|
24188
|
+
printContent(context, content, renderer);
|
|
24189
|
+
onContent?.(content);
|
|
23968
24190
|
}
|
|
23969
24191
|
return highestSeq;
|
|
23970
24192
|
}
|
|
23971
24193
|
function followSession(input) {
|
|
23972
24194
|
let highestSeq = input.seenThroughSeq;
|
|
24195
|
+
const renderer = input.renderer ?? createSessionEventRenderer();
|
|
23973
24196
|
return input.transport.onUpdate((update) => {
|
|
23974
24197
|
input.onUpdate?.(update);
|
|
23975
24198
|
const message = readNewMessage(update, input.sessionId);
|
|
23976
|
-
if (!message) return;
|
|
23977
|
-
if (message.localId && input.ownLocalIds?.has(message.localId)) return;
|
|
24199
|
+
if (!message || message.localId && input.ownLocalIds?.has(message.localId)) return;
|
|
23978
24200
|
if (message.seq !== 0 && message.seq <= highestSeq) return;
|
|
23979
24201
|
highestSeq = Math.max(highestSeq, message.seq);
|
|
23980
24202
|
const content = decodeContent(input.transport, message.ciphertext);
|
|
23981
24203
|
if (!content) return;
|
|
23982
|
-
input.onContent ? input.onContent(content) : printContent(input.context, content);
|
|
24204
|
+
input.onContent ? input.onContent(content) : printContent(input.context, content, renderer);
|
|
23983
24205
|
});
|
|
23984
24206
|
}
|
|
23985
24207
|
async function runAttach(context, options) {
|
|
23986
24208
|
await withTransport(context, async (transport) => {
|
|
23987
|
-
const
|
|
23988
|
-
|
|
24209
|
+
const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
|
|
24210
|
+
const seenThroughSeq = await prerollHistory(context, transport, options.sessionId, options.since, void 0, renderer);
|
|
24211
|
+
followSession({ context, transport, sessionId: options.sessionId, seenThroughSeq, renderer });
|
|
23989
24212
|
context.out.raw(`Attached to ${options.sessionId}. Ctrl+C to detach — the session keeps running.`);
|
|
23990
24213
|
await new Promise((resolve) => {
|
|
23991
24214
|
process.once("SIGINT", () => {
|
|
@@ -23998,17 +24221,20 @@ async function runAttach(context, options) {
|
|
|
23998
24221
|
function createChatState() {
|
|
23999
24222
|
return { phase: "idle", pendingPermission: null };
|
|
24000
24223
|
}
|
|
24001
|
-
function
|
|
24002
|
-
if (content.role !== "session") return
|
|
24003
|
-
const {
|
|
24004
|
-
|
|
24224
|
+
function turnBoundary(content) {
|
|
24225
|
+
if (content.role !== "session") return null;
|
|
24226
|
+
const { role, ev } = content.content;
|
|
24227
|
+
if (role === "agent" && ev.t === "turn-start") return "start";
|
|
24228
|
+
return ev.t === "turn-end" || ev.t === "stop" ? "end" : null;
|
|
24005
24229
|
}
|
|
24006
24230
|
function reduceChatState(state, event) {
|
|
24007
24231
|
switch (event.type) {
|
|
24008
24232
|
case "message-sent":
|
|
24009
24233
|
return { phase: "responding", pendingPermission: null };
|
|
24010
24234
|
case "content": {
|
|
24011
|
-
|
|
24235
|
+
const boundary = turnBoundary(event.content);
|
|
24236
|
+
if (boundary === "start") return { phase: "responding", pendingPermission: null };
|
|
24237
|
+
if (boundary !== "end") return state;
|
|
24012
24238
|
if (state.phase === "idle") return state;
|
|
24013
24239
|
return { phase: "idle", pendingPermission: null };
|
|
24014
24240
|
}
|
|
@@ -24031,6 +24257,27 @@ function interruptMenuChoice(key) {
|
|
|
24031
24257
|
if (first === "s") return "stop";
|
|
24032
24258
|
return "watch";
|
|
24033
24259
|
}
|
|
24260
|
+
function chatPresentation(state) {
|
|
24261
|
+
if (state.phase === "idle") {
|
|
24262
|
+
return {
|
|
24263
|
+
inputMode: "prompt",
|
|
24264
|
+
status: "● Ready — you can type",
|
|
24265
|
+
prompt: "› "
|
|
24266
|
+
};
|
|
24267
|
+
}
|
|
24268
|
+
if (state.phase === "awaiting-permission") {
|
|
24269
|
+
return {
|
|
24270
|
+
inputMode: "permission",
|
|
24271
|
+
status: "◆ Action required — permission response only",
|
|
24272
|
+
prompt: null
|
|
24273
|
+
};
|
|
24274
|
+
}
|
|
24275
|
+
return {
|
|
24276
|
+
inputMode: "locked",
|
|
24277
|
+
status: "● Agent is responding — input locked · Ctrl+C for options",
|
|
24278
|
+
prompt: null
|
|
24279
|
+
};
|
|
24280
|
+
}
|
|
24034
24281
|
function describePermissionRequest(request) {
|
|
24035
24282
|
const summary = summarizeToolArgs(request.arguments ?? {});
|
|
24036
24283
|
if (summary) return `${request.toolName}(${summary})`;
|
|
@@ -24061,6 +24308,106 @@ function resumeHistoryCount(input) {
|
|
|
24061
24308
|
if (!input.resuming) return 0;
|
|
24062
24309
|
return input.since ?? DEFAULT_RESUME_HISTORY;
|
|
24063
24310
|
}
|
|
24311
|
+
class ChatInputController {
|
|
24312
|
+
constructor(readline, input) {
|
|
24313
|
+
this.readline = readline;
|
|
24314
|
+
this.input = input;
|
|
24315
|
+
this.readline.once("close", this.handleClose);
|
|
24316
|
+
}
|
|
24317
|
+
closed = false;
|
|
24318
|
+
locked = false;
|
|
24319
|
+
rawBeforeLock = false;
|
|
24320
|
+
onInterrupt = null;
|
|
24321
|
+
suspendedDataListeners = [];
|
|
24322
|
+
keyListener = null;
|
|
24323
|
+
resolveKey = null;
|
|
24324
|
+
questionAbort = null;
|
|
24325
|
+
handleClose = () => {
|
|
24326
|
+
this.closed = true;
|
|
24327
|
+
this.cancelQuestion();
|
|
24328
|
+
this.unlock();
|
|
24329
|
+
};
|
|
24330
|
+
handleLockedData = (chunk) => {
|
|
24331
|
+
const value2 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
24332
|
+
if (value2.includes("")) this.onInterrupt?.();
|
|
24333
|
+
};
|
|
24334
|
+
lock(onInterrupt) {
|
|
24335
|
+
this.onInterrupt = onInterrupt;
|
|
24336
|
+
if (this.locked || this.closed) return;
|
|
24337
|
+
this.cancelQuestion();
|
|
24338
|
+
this.locked = true;
|
|
24339
|
+
this.rawBeforeLock = this.input.isRaw === true;
|
|
24340
|
+
this.readline.pause();
|
|
24341
|
+
this.suspendedDataListeners = this.input.listeners("data");
|
|
24342
|
+
for (const listener of this.suspendedDataListeners) this.input.off("data", listener);
|
|
24343
|
+
if (this.input.isTTY) this.input.setRawMode?.(true);
|
|
24344
|
+
this.input.resume();
|
|
24345
|
+
this.input.on("data", this.handleLockedData);
|
|
24346
|
+
}
|
|
24347
|
+
unlock() {
|
|
24348
|
+
if (!this.locked) return;
|
|
24349
|
+
this.locked = false;
|
|
24350
|
+
this.input.off("data", this.handleLockedData);
|
|
24351
|
+
if (this.keyListener) {
|
|
24352
|
+
this.input.off("data", this.keyListener);
|
|
24353
|
+
this.keyListener = null;
|
|
24354
|
+
const resolve = this.resolveKey;
|
|
24355
|
+
this.resolveKey = null;
|
|
24356
|
+
resolve?.("");
|
|
24357
|
+
}
|
|
24358
|
+
if (this.input.isTTY) this.input.setRawMode?.(this.rawBeforeLock);
|
|
24359
|
+
for (const listener of this.suspendedDataListeners) this.input.on("data", listener);
|
|
24360
|
+
this.suspendedDataListeners = [];
|
|
24361
|
+
this.readline.resume();
|
|
24362
|
+
}
|
|
24363
|
+
ask(prompt) {
|
|
24364
|
+
this.unlock();
|
|
24365
|
+
if (this.closed) return Promise.resolve(null);
|
|
24366
|
+
const abort = new AbortController();
|
|
24367
|
+
this.questionAbort = abort;
|
|
24368
|
+
return new Promise((resolve) => {
|
|
24369
|
+
let settled = false;
|
|
24370
|
+
const finish = (answer) => {
|
|
24371
|
+
if (settled) return;
|
|
24372
|
+
settled = true;
|
|
24373
|
+
this.readline.off("close", onClose);
|
|
24374
|
+
abort.signal.removeEventListener("abort", onAbort);
|
|
24375
|
+
if (this.questionAbort === abort) this.questionAbort = null;
|
|
24376
|
+
resolve(answer);
|
|
24377
|
+
};
|
|
24378
|
+
const onClose = () => finish(null);
|
|
24379
|
+
const onAbort = () => finish(null);
|
|
24380
|
+
this.readline.once("close", onClose);
|
|
24381
|
+
abort.signal.addEventListener("abort", onAbort, { once: true });
|
|
24382
|
+
this.readline.question(prompt, { signal: abort.signal }, (answer) => finish(answer));
|
|
24383
|
+
});
|
|
24384
|
+
}
|
|
24385
|
+
cancelQuestion() {
|
|
24386
|
+
this.questionAbort?.abort();
|
|
24387
|
+
}
|
|
24388
|
+
readKey() {
|
|
24389
|
+
if (!this.locked || this.closed) return Promise.resolve("");
|
|
24390
|
+
this.input.off("data", this.handleLockedData);
|
|
24391
|
+
return new Promise((resolve) => {
|
|
24392
|
+
this.resolveKey = resolve;
|
|
24393
|
+
this.keyListener = (chunk) => {
|
|
24394
|
+
if (!this.keyListener) return;
|
|
24395
|
+
this.input.off("data", this.keyListener);
|
|
24396
|
+
this.keyListener = null;
|
|
24397
|
+
this.resolveKey = null;
|
|
24398
|
+
if (this.locked) this.input.on("data", this.handleLockedData);
|
|
24399
|
+
resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
24400
|
+
};
|
|
24401
|
+
this.input.on("data", this.keyListener);
|
|
24402
|
+
});
|
|
24403
|
+
}
|
|
24404
|
+
close() {
|
|
24405
|
+
this.cancelQuestion();
|
|
24406
|
+
this.unlock();
|
|
24407
|
+
this.closed = true;
|
|
24408
|
+
this.readline.off("close", this.handleClose);
|
|
24409
|
+
}
|
|
24410
|
+
}
|
|
24064
24411
|
function readString$1(source, key) {
|
|
24065
24412
|
const value2 = source?.[key];
|
|
24066
24413
|
return typeof value2 === "string" && value2 ? value2 : void 0;
|
|
@@ -24312,32 +24659,6 @@ async function spawnSession(transport, machineId, directory, agent) {
|
|
|
24312
24659
|
}
|
|
24313
24660
|
return result.sessionId;
|
|
24314
24661
|
}
|
|
24315
|
-
function ask(rl, question, state) {
|
|
24316
|
-
if (state.closed) return Promise.resolve(null);
|
|
24317
|
-
return new Promise((resolve) => {
|
|
24318
|
-
const onClose = () => resolve(null);
|
|
24319
|
-
rl.once("close", onClose);
|
|
24320
|
-
rl.question(question, (answer) => {
|
|
24321
|
-
rl.off("close", onClose);
|
|
24322
|
-
resolve(answer);
|
|
24323
|
-
});
|
|
24324
|
-
});
|
|
24325
|
-
}
|
|
24326
|
-
function readSingleKey(rl, input) {
|
|
24327
|
-
rl.pause();
|
|
24328
|
-
const wasRaw = input.isRaw === true;
|
|
24329
|
-
if (input.isTTY) input.setRawMode(true);
|
|
24330
|
-
input.resume();
|
|
24331
|
-
return new Promise((resolve) => {
|
|
24332
|
-
const onData = (chunk) => {
|
|
24333
|
-
input.off("data", onData);
|
|
24334
|
-
if (input.isTTY) input.setRawMode(wasRaw);
|
|
24335
|
-
rl.resume();
|
|
24336
|
-
resolve(typeof chunk === "string" ? chunk : chunk.toString("utf8"));
|
|
24337
|
-
};
|
|
24338
|
-
input.on("data", onData);
|
|
24339
|
-
});
|
|
24340
|
-
}
|
|
24341
24662
|
async function replyToPermission(transport, sessionId, request, answer) {
|
|
24342
24663
|
const approved = answer !== "deny";
|
|
24343
24664
|
await transport.rpc(sessionId, "permission", {
|
|
@@ -24414,50 +24735,22 @@ async function runChat(context, options, providedTransport) {
|
|
|
24414
24735
|
return withTransport(context, async (transport) => {
|
|
24415
24736
|
const sessionId = options.sessionId ?? await spawnSession(transport, target.machineId, target.directory, options.agent);
|
|
24416
24737
|
context.out.raw(`Session ${sessionId}`);
|
|
24417
|
-
context.out.raw("
|
|
24738
|
+
context.out.raw("During a response, typing is locked. Ctrl+C opens response options.");
|
|
24418
24739
|
context.out.raw("At the prompt: /detach to leave with the agent still running · /exit to leave.\n");
|
|
24419
24740
|
const chat = new ChatSession();
|
|
24741
|
+
const renderer = createSessionEventRenderer(terminalContentWidth(process.stdout.columns));
|
|
24420
24742
|
const answered = /* @__PURE__ */ new Set();
|
|
24421
24743
|
const ownLocalIds = /* @__PURE__ */ new Set();
|
|
24422
24744
|
const seenThroughSeq = await prerollHistory(
|
|
24423
24745
|
context,
|
|
24424
24746
|
transport,
|
|
24425
24747
|
sessionId,
|
|
24426
|
-
resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} })
|
|
24748
|
+
resumeHistoryCount({ resuming, ...options.since !== void 0 ? { since: options.since } : {} }),
|
|
24749
|
+
(content) => chat.apply({ type: "content", content }),
|
|
24750
|
+
renderer
|
|
24427
24751
|
);
|
|
24428
|
-
const stopFollowing = followSession({
|
|
24429
|
-
context,
|
|
24430
|
-
transport,
|
|
24431
|
-
sessionId,
|
|
24432
|
-
seenThroughSeq,
|
|
24433
|
-
ownLocalIds,
|
|
24434
|
-
onContent: (content) => {
|
|
24435
|
-
printContent(context, content);
|
|
24436
|
-
chat.apply({ type: "content", content });
|
|
24437
|
-
},
|
|
24438
|
-
onUpdate: (update) => {
|
|
24439
|
-
const cipher = readSessionAgentState(update, sessionId);
|
|
24440
|
-
if (!cipher) return;
|
|
24441
|
-
let decoded;
|
|
24442
|
-
try {
|
|
24443
|
-
decoded = transport.decodePayload(cipher);
|
|
24444
|
-
} catch {
|
|
24445
|
-
return;
|
|
24446
|
-
}
|
|
24447
|
-
for (const request of pendingPermissionRequestsFromState(decoded)) {
|
|
24448
|
-
if (answered.has(request.requestId)) continue;
|
|
24449
|
-
chat.apply({ type: "permission-requested", request });
|
|
24450
|
-
}
|
|
24451
|
-
}
|
|
24452
|
-
});
|
|
24453
24752
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
24454
|
-
const
|
|
24455
|
-
rl.once("close", () => {
|
|
24456
|
-
stdinState.closed = true;
|
|
24457
|
-
});
|
|
24458
|
-
rl.on("line", (raw) => {
|
|
24459
|
-
if (raw.trim() === DETACH_COMMAND) chat.detach();
|
|
24460
|
-
});
|
|
24753
|
+
const input = new ChatInputController(rl, process.stdin);
|
|
24461
24754
|
let menuOpen = false;
|
|
24462
24755
|
const onSigint = () => {
|
|
24463
24756
|
if (interruptDecision(chat.current) === "exit-cli") {
|
|
@@ -24469,7 +24762,7 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
24469
24762
|
if (menuOpen) return;
|
|
24470
24763
|
menuOpen = true;
|
|
24471
24764
|
context.out.raw("\n^C [Enter] keep watching · [d] detach, agent keeps working · [s] stop the response");
|
|
24472
|
-
void
|
|
24765
|
+
void input.readKey().then((key) => {
|
|
24473
24766
|
menuOpen = false;
|
|
24474
24767
|
const choice = interruptMenuChoice(key);
|
|
24475
24768
|
if (choice === "detach") {
|
|
@@ -24484,12 +24777,72 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
24484
24777
|
context.out.raw("…still working.");
|
|
24485
24778
|
});
|
|
24486
24779
|
};
|
|
24780
|
+
const stopFollowing = followSession({
|
|
24781
|
+
context,
|
|
24782
|
+
transport,
|
|
24783
|
+
sessionId,
|
|
24784
|
+
seenThroughSeq,
|
|
24785
|
+
ownLocalIds,
|
|
24786
|
+
renderer,
|
|
24787
|
+
onContent: (content) => {
|
|
24788
|
+
printContent(context, content, renderer);
|
|
24789
|
+
chat.apply({ type: "content", content });
|
|
24790
|
+
if (chat.current.phase === "responding") input.lock(onSigint);
|
|
24791
|
+
},
|
|
24792
|
+
onUpdate: (update) => {
|
|
24793
|
+
const cipher = readSessionAgentState(update, sessionId);
|
|
24794
|
+
if (!cipher) return;
|
|
24795
|
+
let decoded;
|
|
24796
|
+
try {
|
|
24797
|
+
decoded = transport.decodePayload(cipher);
|
|
24798
|
+
} catch {
|
|
24799
|
+
return;
|
|
24800
|
+
}
|
|
24801
|
+
for (const request of pendingPermissionRequestsFromState(decoded)) {
|
|
24802
|
+
if (answered.has(request.requestId)) continue;
|
|
24803
|
+
chat.apply({ type: "permission-requested", request });
|
|
24804
|
+
input.cancelQuestion();
|
|
24805
|
+
}
|
|
24806
|
+
}
|
|
24807
|
+
});
|
|
24487
24808
|
process.on("SIGINT", onSigint);
|
|
24488
24809
|
let outcome = "exited";
|
|
24489
24810
|
try {
|
|
24490
24811
|
for (; ; ) {
|
|
24491
|
-
|
|
24492
|
-
|
|
24812
|
+
if (chat.current.phase !== "idle") {
|
|
24813
|
+
if (chat.current.phase === "responding") {
|
|
24814
|
+
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
24815
|
+
input.lock(onSigint);
|
|
24816
|
+
}
|
|
24817
|
+
for (; ; ) {
|
|
24818
|
+
const state = await chat.waitForAttention();
|
|
24819
|
+
if (chat.isDetached) break;
|
|
24820
|
+
if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
|
|
24821
|
+
const request = state.pendingPermission;
|
|
24822
|
+
context.out.line({ style: "tool", text: chatPresentation(state).status });
|
|
24823
|
+
let answer = parsePermissionAnswer(await input.ask(`${permissionPromptText(request)} `) ?? "n");
|
|
24824
|
+
while (answer === "unknown") {
|
|
24825
|
+
answer = parsePermissionAnswer(await input.ask("Please answer y, n, or a. ") ?? "n");
|
|
24826
|
+
}
|
|
24827
|
+
answered.add(request.requestId);
|
|
24828
|
+
await replyToPermission(transport, sessionId, request, answer);
|
|
24829
|
+
chat.apply({ type: "permission-answered" });
|
|
24830
|
+
context.out.line({ style: "thinking", text: chatPresentation(chat.current).status });
|
|
24831
|
+
input.lock(onSigint);
|
|
24832
|
+
}
|
|
24833
|
+
if (chat.isDetached) {
|
|
24834
|
+
outcome = "detached";
|
|
24835
|
+
break;
|
|
24836
|
+
}
|
|
24837
|
+
continue;
|
|
24838
|
+
}
|
|
24839
|
+
const ready = chatPresentation(chat.current);
|
|
24840
|
+
context.out.line({ style: "status", text: ready.status });
|
|
24841
|
+
const line = await input.ask(ready.prompt);
|
|
24842
|
+
if (line === null) {
|
|
24843
|
+
if (chat.current.phase !== "idle") continue;
|
|
24844
|
+
break;
|
|
24845
|
+
}
|
|
24493
24846
|
const prompt = line.trim();
|
|
24494
24847
|
if (prompt === "/exit" || prompt === "/quit") break;
|
|
24495
24848
|
if (prompt === DETACH_COMMAND) {
|
|
@@ -24497,30 +24850,14 @@ Leaving. Reattach with \`saycode chat -s ${sessionId}\`.`);
|
|
|
24497
24850
|
break;
|
|
24498
24851
|
}
|
|
24499
24852
|
if (prompt === "") continue;
|
|
24500
|
-
ownLocalIds.add(await sendPrompt(context, sessionId, prompt));
|
|
24501
24853
|
chat.apply({ type: "message-sent" });
|
|
24502
|
-
|
|
24503
|
-
|
|
24504
|
-
const state = await chat.waitForAttention();
|
|
24505
|
-
if (chat.isDetached) break;
|
|
24506
|
-
if (state.phase !== "awaiting-permission" || !state.pendingPermission) break;
|
|
24507
|
-
const request = state.pendingPermission;
|
|
24508
|
-
let answer = parsePermissionAnswer(await ask(rl, `${permissionPromptText(request)} `, stdinState) ?? "n");
|
|
24509
|
-
while (answer === "unknown") {
|
|
24510
|
-
answer = parsePermissionAnswer(await ask(rl, "Please answer y, n, or a. ", stdinState) ?? "n");
|
|
24511
|
-
}
|
|
24512
|
-
answered.add(request.requestId);
|
|
24513
|
-
await replyToPermission(transport, sessionId, request, answer);
|
|
24514
|
-
chat.apply({ type: "permission-answered" });
|
|
24515
|
-
}
|
|
24516
|
-
if (chat.isDetached) {
|
|
24517
|
-
outcome = "detached";
|
|
24518
|
-
break;
|
|
24519
|
-
}
|
|
24854
|
+
input.lock(onSigint);
|
|
24855
|
+
ownLocalIds.add(await sendPrompt(context, sessionId, prompt));
|
|
24520
24856
|
}
|
|
24521
24857
|
} finally {
|
|
24522
24858
|
stopFollowing();
|
|
24523
24859
|
process.off("SIGINT", onSigint);
|
|
24860
|
+
input.close();
|
|
24524
24861
|
rl.close();
|
|
24525
24862
|
}
|
|
24526
24863
|
context.out.raw(outcome === "detached" ? `
|