Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1642-folded-thinking-preview.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The folded thinking row's one-line preview now skips fences and horizontal rules, cuts on a character boundary instead of halving an emoji, and ends a clipped line with an ellipsis; reasoning that is nothing but markup says how many lines it has instead of showing a fence fragment ([#1642](https://github.com/ThinkFlowLab/FlyBot/issues/1642)).
21 changes: 19 additions & 2 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@ var ZH = {
"Message actions": "消息操作",
Download: "下载",
"Thought it through": "思考过程",
"{n} line of reasoning": "{n} 行思考内容",
"{n} lines of reasoning": "{n} 行思考内容",
"Seen — I’ll get to this right after the current step.": "看到了,这步做完就接着处理你这条。",
"Earlier messages": "更早的消息",
"Loading earlier messages…": "正在加载更早的消息…",
Expand Down Expand Up @@ -6171,9 +6173,24 @@ function MessageTools({ text, name, sessionId, react, actions }) {
);
}
var PEEK_MAX = 160;
var CUT = "…";
var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/];
var isMarkup = (line2) => MARKUP.some((pattern) => pattern.test(line2));
var graphemes = (line2) => typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? [...new Intl.Segmenter().segment(line2)].map((part) => part.segment) : [...line2];
var clip = (line2) => {
if (line2.length <= PEEK_MAX) return line2;
let kept = "";
for (const part of graphemes(line2)) {
if (kept.length + part.length > PEEK_MAX - CUT.length) break;
kept += part;
}
return kept + CUT;
};
var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length });
var peekLine = (text) => {
const line2 = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).find((part) => part !== "");
return line2 === void 0 ? "" : line2.slice(0, PEEK_MAX);
const lines = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== "");
const prose = lines.find((line2) => !isMarkup(line2));
return prose === void 0 ? onlyMarkup(lines) : clip(prose);
};
function ThinkingRow({ text }) {
if (typeof text !== "string" || text.trim() === "") return null;
Expand Down
28 changes: 26 additions & 2 deletions core/src/client/cells.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,33 @@ function MessageTools({ text, name, sessionId, react, actions }) {
// The first line with something on it, inner runs of whitespace squeezed: a folded row shows it, so
// the row never reads as an empty frame while the text it names sits behind the caret (#1624).
const PEEK_MAX = 160
const CUT = '…'
// A fence (```js, ~~~, a closing fence) and a horizontal rule are markup, not something thought, so
// the preview skips them and takes the first line that has prose (#1642).
const MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]
const isMarkup = (line) => MARKUP.some(pattern => pattern.test(line))
// Graphemes where the platform segments them, code points otherwise: either way the cut never lands
// inside a surrogate pair, so a clipped emoji cannot show up as U+FFFD (#1642).
const graphemes = (line) => typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
? [...new Intl.Segmenter().segment(line)].map(part => part.segment)
: [...line]
const clip = (line) => {
if (line.length <= PEEK_MAX) return line
let kept = ''
for (const part of graphemes(line)) {
if (kept.length + part.length > PEEK_MAX - CUT.length) break
kept += part
}
return kept + CUT
}
// Nothing but markup: a line count still says something, where a fence fragment says nothing (#1642).
const onlyMarkup = (lines) => (lines.length === 1
? t('{n} line of reasoning', { n: lines.length })
: t('{n} lines of reasoning', { n: lines.length }))
const peekLine = (text) => {
const line = text.split('\n').map((part) => part.replace(/\s+/g, ' ').trim()).find((part) => part !== '')
return line === undefined ? '' : line.slice(0, PEEK_MAX)
const lines = text.split('\n').map((part) => part.replace(/\s+/g, ' ').trim()).filter((part) => part !== '')
const prose = lines.find((line) => !isMarkup(line))
return prose === undefined ? onlyMarkup(lines) : clip(prose)
}

function ThinkingRow({ text }) {
Expand Down
2 changes: 2 additions & 0 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,8 @@ const ZH = {
'Message actions': '消息操作',
Download: '下载',
'Thought it through': '思考过程',
'{n} line of reasoning': '{n} 行思考内容',
'{n} lines of reasoning': '{n} 行思考内容',
'Seen — I’ll get to this right after the current step.': '看到了,这步做完就接着处理你这条。',
'Earlier messages': '更早的消息',
'Loading earlier messages…': '正在加载更早的消息…',
Expand Down
40 changes: 40 additions & 0 deletions core/test/thinking-row.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,43 @@ test('#1636: an opened row hides its preview, so its first line is not printed t
/\.bt-think\[open\] \.bt-think-peek\{display:none\}/,
)
})

test('#1642: a fence or a rule is not the peek line; the first prose line is', () => {
const fenced = [think('```js\nconst x = 1\n```')]
const peek = classed(thinkingRow(render(step(1, fenced), [step(1, fenced)])), 'bt-think-peek')
assert.ok(peek, 'the closed row carries a line of the reasoning')
assert.equal(text(peek), 'const x = 1', 'markup is not what the Bot was thinking')
assert.doesNotMatch(text(peek), /```/)

const ruled = [think('---\n***\nLet me read it\n___')]
const second = classed(thinkingRow(render(step(1, ruled), [step(1, ruled)])), 'bt-think-peek')
assert.ok(second)
assert.equal(text(second), 'Let me read it', 'a horizontal rule is markup too')
})

test('#1642: a cut preview keeps whole characters and says it was cut', () => {
const long = [think(`${'a'.repeat(150)}😀${'b'.repeat(100)}`)]
const peek = classed(thinkingRow(render(step(1, long), [step(1, long)])), 'bt-think-peek')
assert.ok(peek)
const shown = text(peek)
assert.equal(shown.length, 160, 'the cap still holds')
assert.ok(shown.includes('😀'), 'an emoji inside the cap survives whole')
assert.ok(shown.endsWith('…'), 'a clipped line says it was clipped')
assert.doesNotMatch(shown, /[\uD800-\uDFFF]/u, 'no half a surrogate pair')

// The reported input: a pair cut exactly at the cap used to render as U+FFFD.
const edge = [think(`${'a'.repeat(159)}😀 tail`)]
const cut = text(classed(thinkingRow(render(step(1, edge), [step(1, edge)])), 'bt-think-peek'))
assert.equal(cut.length, 160)
assert.ok(cut.endsWith('…'))
assert.doesNotMatch(cut, /[\uD800-\uDFFF]/u, 'no half a surrogate pair at the cap either')
})

test('#1642: reasoning that is only markup gets a neutral summary, never a bare fence or an empty row', () => {
const onlyMarkup = [think('```js\n```')]
const details = thinkingRow(render(step(1, onlyMarkup), [step(1, onlyMarkup)]))
assert.ok(details, 'the row stays')
const peek = classed(details, 'bt-think-peek')
assert.ok(peek, 'the folded row never reads as an empty frame')
assert.equal(text(peek), '2 lines of reasoning')
})
4 changes: 3 additions & 1 deletion patches/ds-bot/0001-core-fixes.patch
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
// Command palette
Messages: '消息',
Commands: '命令',
@@ -390,10 +390,12 @@ const ZH = {
@@ -390,10 +390,14 @@ const ZH = {
Reply: '回复',
'More message actions': '更多操作',
More: '更多',
Expand All @@ -1161,6 +1161,8 @@ diff --git a/src/client/i18n.js b/src/client/i18n.js
'Message actions': '消息操作',
Download: '下载',
'Thought it through': '思考过程',
+ '{n} line of reasoning': '{n} 行思考内容',
+ '{n} lines of reasoning': '{n} 行思考内容',
+ 'Seen — I’ll get to this right after the current step.': '看到了,这步做完就接着处理你这条。',
'Earlier messages': '更早的消息',
'Loading earlier messages…': '正在加载更早的消息…',
Expand Down
30 changes: 27 additions & 3 deletions patches/ds-bot/0003-flybot-api-client.patch
Original file line number Diff line number Diff line change
Expand Up @@ -529,16 +529,40 @@ diff --git a/src/client/cells.js b/src/client/cells.js
// Closes a popover on a press outside the given elements or on Escape.
function useDismiss(open, close, refs) {
useEffect(() => {
@@ -100,9 +100,21 @@ function MessageTools({ text, name, sessionId, react, actions }) {
@@ -100,9 +100,45 @@ function MessageTools({ text, name, sessionId, react, actions }) {
item(t('Copy'), CopyIcon, copy), item(t('Download'), DownloadIcon, download)), document.body) : null)
}

+// The first line with something on it, inner runs of whitespace squeezed: a folded row shows it, so
+// the row never reads as an empty frame while the text it names sits behind the caret (#1624).
+const PEEK_MAX = 160
+const CUT = '…'
+// A fence (```js, ~~~, a closing fence) and a horizontal rule are markup, not something thought, so
+// the preview skips them and takes the first line that has prose (#1642).
+const MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/]
+const isMarkup = (line) => MARKUP.some(pattern => pattern.test(line))
+// Graphemes where the platform segments them, code points otherwise: either way the cut never lands
+// inside a surrogate pair, so a clipped emoji cannot show up as U+FFFD (#1642).
+const graphemes = (line) => typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
+ ? [...new Intl.Segmenter().segment(line)].map(part => part.segment)
+ : [...line]
+const clip = (line) => {
+ if (line.length <= PEEK_MAX) return line
+ let kept = ''
+ for (const part of graphemes(line)) {
+ if (kept.length + part.length > PEEK_MAX - CUT.length) break
+ kept += part
+ }
+ return kept + CUT
+}
+// Nothing but markup: a line count still says something, where a fence fragment says nothing (#1642).
+const onlyMarkup = (lines) => (lines.length === 1
+ ? t('{n} line of reasoning', { n: lines.length })
+ : t('{n} lines of reasoning', { n: lines.length }))
+const peekLine = (text) => {
+ const line = text.split('\n').map((part) => part.replace(/\s+/g, ' ').trim()).find((part) => part !== '')
+ return line === undefined ? '' : line.slice(0, PEEK_MAX)
+ const lines = text.split('\n').map((part) => part.replace(/\s+/g, ' ').trim()).filter((part) => part !== '')
+ const prose = lines.find((line) => !isMarkup(line))
+ return prose === undefined ? onlyMarkup(lines) : clip(prose)
+}
+
function ThinkingRow({ text }) {
Expand Down
105 changes: 61 additions & 44 deletions patches/ds-bot/0008-client-bundle.patch
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ diff --git a/client.js b/client.js
// Command palette
Messages: "消息",
Commands: "命令",
@@ -446,10 +446,12 @@ var ZH = {
@@ -446,10 +446,14 @@ var ZH = {
Reply: "回复",
"More message actions": "更多操作",
More: "更多",
Expand All @@ -581,6 +581,8 @@ diff --git a/client.js b/client.js
"Message actions": "消息操作",
Download: "下载",
"Thought it through": "思考过程",
+ "{n} line of reasoning": "{n} 行思考内容",
+ "{n} lines of reasoning": "{n} 行思考内容",
+ "Seen — I’ll get to this right after the current step.": "看到了,这步做完就接着处理你这条。",
"Earlier messages": "更早的消息",
"Loading earlier messages…": "正在加载更早的消息…",
Expand Down Expand Up @@ -3668,7 +3670,7 @@ diff --git a/client.js b/client.js
.bt-ububble{max-width:min(560px,78%);background:var(--bt-user);color:var(--bt-user-ink);border-radius:18px;padding:7px 12px;font-size:14px;line-height:20px;box-sizing:border-box;overflow-wrap:anywhere}
.bt-ububble [class*="_markdown"],.bt-ububble a{color:inherit!important}
.bt-ububble p{margin:0;line-height:22px}
@@ -2805,1975 +2805,4080 @@ function sheetClassNames(sheet) {
@@ -2805,1975 +2805,4095 @@ function sheetClassNames(sheet) {
visit(sheet.cssRules);
} catch {
}
Expand Down Expand Up @@ -5172,19 +5174,12 @@ diff --git a/client.js b/client.js
- ) : null,
- foot
+ error ? (0, import_react8.createElement)("div", { className: "bt-flybot-approval-error", role: "alert" }, error) : null
);
+ );
+ return (0, import_react8.createElement)(
+ "div",
+ { className: "bt-held-actions", "data-flybot": "held-reply-actions" },
+ custom ? (0, import_react8.createElement)(boundary(), { id: custom.id, fallback: plain }, (0, import_react8.createElement)(custom.component, { held, decide, call: call3, actions })) : plain
+ );
+}
+
+// src/client/turn-reasoning.js
+function turnReasoning(steps, current) {
+ const members = steps.length ? steps : [current];
+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
);
}
-function SettledQuestion({ question, answer }) {
- const options = optionsOf(question);
Expand All @@ -5203,6 +5198,13 @@ diff --git a/client.js b/client.js
- );
- }
- return (0, import_react4.createElement)(
+
+// src/client/turn-reasoning.js
+function turnReasoning(steps, current) {
+ const members = steps.length ? steps : [current];
+ if (members.find((step) => stepReasoning(step) !== "")?.step !== current.step) return "";
+ return members.filter((step) => !thinkingLive(step)).map(stepReasoning).filter(Boolean).join("\n\n");
+}
+var texts = (step) => step.blocks.filter((block) => block.kind === "text" && block.text.trim() !== "");
+var thinkingLive = (step) => step.status === "running" && texts(step).length === 0 && step.blocks.some((block) => block.kind === "reasoning");
+var stepReasoning = (step) => step.blocks.filter((block) => block.kind === "reasoning").map((block) => block.text).join("\n").trim();
Expand Down Expand Up @@ -5388,49 +5390,31 @@ diff --git a/client.js b/client.js
- const loading = useSession?.(historyLoading) === true;
- if (question === void 0 || question.callId && (drawn || loading)) return null;
- return (0, import_react4.createElement)(PendingQuestion, { sessionId, question, actions, inline: false });
+var PEEK_MAX = 160;
+var peekLine = (text) => {
+ const line2 = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).find((part) => part !== "");
+ return line2 === void 0 ? "" : line2.slice(0, PEEK_MAX);
+};
+function ThinkingRow({ text }) {
+ if (typeof text !== "string" || text.trim() === "") return null;
+ const peek = peekLine(text);
+ return (0, import_react9.createElement)(
+ "details",
+ { className: "bt-think" },
+ (0, import_react9.createElement)(
+ "summary",
+ null,
+ (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"),
+ t("Thought it through"),
+ peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null
+ ),
+ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text)
+ );
}
-}
-function AnsweredCell({ node }) {
- const data = node.data ?? {};
- const questions = Array.isArray(data.questions) ? data.questions : [];
- if (questions.length === 0) {
- return data.text ? (0, import_react4.createElement)("div", { className: "bt-urow" }, (0, import_react4.createElement)("div", { className: "bt-ububble" }, data.text)) : null;
- }
+var PEEK_MAX = 160;
+var CUT = "…";
+var MARKUP = [/^\s{0,3}(?:`{3,}|~{3,})/, /^\s{0,3}([-*_])(?:\s*\1){2,}\s*$/];
+var isMarkup = (line2) => MARKUP.some((pattern) => pattern.test(line2));
+var graphemes = (line2) => typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? [...new Intl.Segmenter().segment(line2)].map((part) => part.segment) : [...line2];
+var clip = (line2) => {
+ if (line2.length <= PEEK_MAX) return line2;
+ let kept = "";
+ for (const part of graphemes(line2)) {
+ if (kept.length + part.length > PEEK_MAX - CUT.length) break;
+ kept += part;
}
- const answers = Array.isArray(data.answers) ? data.answers : [];
- return (0, import_react4.createElement)("div", { className: "bt-q-inline" }, questions.map((question) => (0, import_react4.createElement)(SettledQuestion, {
- key: question.id,
- question,
- answer: answers.find((answer) => answer.id === question.id) ?? { selected: [] }
- })));
+function useFileMentions({ node, openFile, fileMentions, useTurnData }) {
+ const tail = useTurnData?.("turn-tail");
+ const location2 = node.location;
+ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0;
+ const seq = node.data.finalNode?.seq;
+ return (0, import_react9.useMemo)(() => {
+ if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0;
+ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0;
+ }, [fileMentions, openFile, seq, tail, turn]);
}
-}
-
-// src/client/secret-card.js
-var import_react5 = require("react");
Expand Down Expand Up @@ -5460,7 +5444,8 @@ diff --git a/client.js b/client.js
- if (scope === "all") return t("All Bots");
- const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
- return names.length === 0 ? t("No Bot") : names.join(t(" and "));
-};
+ return kept + CUT;
};
-var when = (time) => Number.isFinite(time) ? new Date(time).toLocaleString(dateLocale(), { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }) : "";
-function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
- return (0, import_react5.createElement)("input", {
Expand All @@ -5485,6 +5470,38 @@ diff --git a/client.js b/client.js
- event.preventDefault();
- onEnter?.();
- }
+var onlyMarkup = (lines) => lines.length === 1 ? t("{n} line of reasoning", { n: lines.length }) : t("{n} lines of reasoning", { n: lines.length });
+var peekLine = (text) => {
+ const lines = text.split("\n").map((part) => part.replace(/\s+/g, " ").trim()).filter((part) => part !== "");
+ const prose = lines.find((line2) => !isMarkup(line2));
+ return prose === void 0 ? onlyMarkup(lines) : clip(prose);
+};
+function ThinkingRow({ text }) {
+ if (typeof text !== "string" || text.trim() === "") return null;
+ const peek = peekLine(text);
+ return (0, import_react9.createElement)(
+ "details",
+ { className: "bt-think" },
+ (0, import_react9.createElement)(
+ "summary",
+ null,
+ (0, import_react9.createElement)("span", { className: "bt-think-caret", "aria-hidden": true }, "›"),
+ t("Thought it through"),
+ peek ? (0, import_react9.createElement)("span", { className: "bt-think-peek" }, peek) : null
+ ),
+ (0, import_react9.createElement)("div", { className: "bt-think-text" }, text)
+ );
+}
+function useFileMentions({ node, openFile, fileMentions, useTurnData }) {
+ const tail = useTurnData?.("turn-tail");
+ const location2 = node.location;
+ const turn = location2?.kind === "turn" || location2?.kind === "step" ? location2.turn : void 0;
+ const seq = node.data.finalNode?.seq;
+ return (0, import_react9.useMemo)(() => {
+ if (typeof fileMentions !== "function" || turn?.status !== "closed" || seq === void 0) return void 0;
+ return tail?.closing?.finalNode.seq === seq ? fileMentions({ turn, seq, openFile }) : void 0;
+ }, [fileMentions, openFile, seq, tail, turn]);
+}
+function useClosingText(node, useTurnData) {
+ const tail = useTurnData?.("turn-tail");
+ const seq = node.data.finalNode?.seq;
Expand Down
Loading
Loading