grok-telegram-bot 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +26 -0
  2. package/CHANGELOG.md +55 -0
  3. package/package.json +1 -1
  4. package/scripts/analyze-jsonl.ts +33 -0
  5. package/scripts/delayed-restart.ps1 +29 -0
  6. package/scripts/probe-exit-response-shape.py +77 -0
  7. package/scripts/probe-plan-exit.py +60 -0
  8. package/scripts/probe-plan-exit2.py +48 -0
  9. package/scripts/probe-plan-fields.py +41 -0
  10. package/scripts/probe-plan-fields2.py +58 -0
  11. package/scripts/probe-plan-response-path.py +48 -0
  12. package/scripts/sample-claude-tooluse.ts +21 -0
  13. package/scripts/sample-kiro-events.ts +31 -0
  14. package/scripts/smoke-exit-plan.ts +274 -0
  15. package/scripts/smoke-exit-shapes.ts +252 -0
  16. package/scripts/smoke-import.mjs +82 -0
  17. package/scripts/smoke-import.ts +73 -0
  18. package/src/app/accounts.ts +84 -0
  19. package/src/app/instance-lock.ts +6 -0
  20. package/src/app/types.ts +19 -2
  21. package/src/app/updater.ts +17 -6
  22. package/src/app/usage.ts +204 -7
  23. package/src/bot/account-rotator.ts +71 -2
  24. package/src/bot/bot.ts +36 -0
  25. package/src/bot/chat-controller.ts +35 -0
  26. package/src/bot/commands.ts +2 -0
  27. package/src/bot/complexity-gate.ts +69 -0
  28. package/src/bot/deps.ts +19 -0
  29. package/src/bot/handlers/accounts.ts +55 -5
  30. package/src/bot/handlers/import-session.ts +290 -0
  31. package/src/bot/handlers/menu.ts +17 -38
  32. package/src/bot/handlers/message.ts +1 -0
  33. package/src/bot/handlers/running.ts +35 -5
  34. package/src/bot/handlers/session-card.ts +12 -0
  35. package/src/bot/handlers/sessions.ts +14 -3
  36. package/src/bot/handlers/usage.ts +118 -16
  37. package/src/bot/menu/keyboard.ts +5 -4
  38. package/src/bot/menu/status-panel.ts +19 -6
  39. package/src/bot/prompt-content.ts +4 -0
  40. package/src/bot/reauth-controller.ts +2 -2
  41. package/src/bot/session-fork.ts +11 -0
  42. package/src/bot/session-runtime.ts +831 -64
  43. package/src/bot/suggestions.ts +429 -0
  44. package/src/config.ts +41 -0
  45. package/src/grok/client.ts +106 -20
  46. package/src/grok/plan-approval.ts +72 -0
  47. package/src/grok/session-log.ts +16 -0
  48. package/src/grok/types.ts +21 -2
  49. package/src/import/build-import.ts +132 -0
  50. package/src/import/history-readers.ts +681 -0
  51. package/src/import/list-running.ts +100 -0
  52. package/src/import/sources.ts +78 -0
  53. package/src/index.ts +179 -24
  54. package/src/render/diff.ts +11 -2
  55. package/src/render/file-summary.ts +31 -1
  56. package/src/render/markdown.ts +293 -35
  57. package/src/render/plan.ts +127 -0
  58. package/src/render/session-comment.ts +261 -0
  59. package/src/render/tool-call-detail.ts +400 -19
  60. package/src/render/tool-call-merge.ts +115 -0
  61. package/src/render/tool-call.ts +405 -142
  62. package/src/render/truncate.ts +85 -0
  63. package/src/service/windows.ts +14 -2
  64. package/src/sessions/history.ts +57 -0
  65. package/src/sessions/store.ts +3 -0
  66. package/src/sessions/types.ts +5 -0
  67. package/src/stream/streamer.ts +73 -9
  68. package/src/tasks/runner.ts +4 -3
@@ -7,7 +7,7 @@ import { type Bot, type Context, InlineKeyboard } from "grammy";
7
7
  import type { RunningSession, SwitchResult } from "../chat-controller.js";
8
8
  import type { BotDeps } from "../deps.js";
9
9
  import type { HistoryEntry } from "../../sessions/types.js";
10
- import { jsonlMtimeMs, readFirstPrompt } from "../../sessions/history.js";
10
+ import { jsonlMtimeMs, readFirstPrompt, readLastCardSummary } from "../../sessions/history.js";
11
11
  import { progressBar } from "../../render/progress.js";
12
12
  import { refreshMenu } from "../menu/refresh.js";
13
13
  import { sendMarkdownDoc } from "../telegram-io.js";
@@ -49,27 +49,47 @@ function cleanPrompt(raw: string): string {
49
49
  }
50
50
 
51
51
  /** Build a rich card (plain text, no MarkdownV2) + buttons for one controlled
52
- * session: Switch / History / Close. */
52
+ * session: Switch / History / Close.
53
+ *
54
+ * The comment line always answers "what is happening / what was solved":
55
+ * busy → live current step (tool / thinking / working on …)
56
+ * idle → last-turn outcome (assistant result + files), never bare import noise
57
+ */
53
58
  function buildRunningCard(s: RunningSession, deps: BotDeps, now: number): { text: string; kb: InlineKeyboard } {
54
59
  const dot = s.foreground ? "\u25B6\uFE0F" : s.busy ? "\u{1F7E0}" : "\u26AA";
55
60
  const state = s.foreground ? "foreground" : s.busy ? "working" : "idle";
56
61
 
57
62
  let when = "new";
58
- let prompt = "";
63
+ let historySummary = "";
64
+ let firstPrompt = "";
59
65
  if (s.sessionId) {
60
66
  const path = deps.store.jsonlPath(s.sessionId);
61
67
  const mtime = jsonlMtimeMs(path);
62
68
  if (mtime) when = timeAgo(now - mtime);
63
- prompt = cleanPrompt(readFirstPrompt(path));
69
+ // Last assistant outcome beats first-prompt import-confirm noise.
70
+ historySummary = readLastCardSummary(path);
71
+ firstPrompt = cleanPrompt(readFirstPrompt(path));
72
+ if (/session import complete/i.test(firstPrompt)) firstPrompt = "";
64
73
  }
65
74
 
75
+ const diskComment = s.sessionId ? deps.store.get(s.sessionId)?.comment?.trim() : undefined;
76
+ // Order: live runtime comment → persisted last-turn summary → history tail → first prompt.
77
+ const comment =
78
+ (s.comment && s.comment.trim()) ||
79
+ diskComment ||
80
+ historySummary ||
81
+ firstPrompt;
82
+
66
83
  const meta = [when, state];
67
84
  if (s.busy) meta.push("\u23F3");
68
85
  if (s.unread > 0) meta.push(`${s.unread} \u{1F4EC} unread`);
69
86
 
87
+ const commentLabel = s.busy ? "\u23F3" : "\u{1F4AC}";
70
88
  const lines = [
71
89
  `${dot} ${s.projectName}`,
72
- prompt ? `\u{1F4AC} \u201C${trunc(prompt, 120)}\u201D` : "\u{1F4AC} (no messages yet)",
90
+ comment
91
+ ? `${commentLabel} ${trunc(comment, 200)}`
92
+ : "\u{1F4AC} (no messages yet)",
73
93
  `\u{1F552} ${meta.join(" \u00B7 ")}`,
74
94
  ];
75
95
  if (s.progress !== undefined) lines.push(`\u{1F4C8} ${progressBar(s.progress)}`);
@@ -171,6 +191,16 @@ async function deliverSwitch(ctx: Context, deps: BotDeps, res: SwitchResult): Pr
171
191
  if (!res.busy && res.rt.lastTurnSummary) {
172
192
  await ctx.reply(res.rt.lastTurnSummary);
173
193
  }
194
+
195
+ // Re-show post-turn suggestions generated while this session was in the
196
+ // background (or if the user missed the Done notify). Buttons stay wired to
197
+ // the same batch ids so taps still submit the follow-up.
198
+ if (!res.busy) {
199
+ const sug = res.rt.peekPendingSuggestions();
200
+ if (sug) {
201
+ await ctx.reply(sug.text, { reply_markup: sug.markup }).catch(() => {});
202
+ }
203
+ }
174
204
  }
175
205
 
176
206
  function fmtEntry(e: HistoryEntry): string {
@@ -10,6 +10,7 @@ import { InlineKeyboard } from "grammy";
10
10
  import { basename } from "node:path";
11
11
  import { progressBar } from "../../render/progress.js";
12
12
  import type { SessionMeta } from "../../sessions/types.js";
13
+ // Note: callers may pass `comment` from runtime or history (last-turn outcome).
13
14
 
14
15
  export interface SessionCardExtras {
15
16
  /** Context-usage %, when the session is loaded in the current ACP process. */
@@ -22,6 +23,11 @@ export interface SessionCardExtras {
22
23
  selfPid?: number;
23
24
  /** Latest task-completion % (0–100) for this session, if this chat runs it. */
24
25
  progress?: number;
26
+ /**
27
+ * Live step (while working) or chat summary (when idle). Overrides
28
+ * `m.comment` when provided by the controlling chat runtime.
29
+ */
30
+ comment?: string;
25
31
  }
26
32
 
27
33
  export interface SessionCard {
@@ -34,9 +40,15 @@ export function buildSessionCard(m: SessionMeta, extra: SessionCardExtras = {}):
34
40
  const dot = m.active ? "\u{1F7E2}" : "\u26AA";
35
41
  const state = m.active ? `running${m.lockPid ? ` \u00B7 pid ${m.lockPid}` : ""}` : "idle";
36
42
  const proj = m.cwd ? basename(m.cwd) : "(no project)";
43
+ const comment = (extra.comment || m.comment || "").trim();
37
44
 
38
45
  const lines = [`${dot} ${m.title}`, `\u{1F4C1} ${proj}`];
39
46
  if (m.cwd) lines.push(` ${m.cwd}`);
47
+ // Always surface "what is happening / what was done" when we have it.
48
+ if (comment) {
49
+ const icon = m.active || typeof extra.progress === "number" ? "\u23F3" : "\u{1F4AC}";
50
+ lines.push(`${icon} ${comment.length > 160 ? comment.slice(0, 159) + "\u2026" : comment}`);
51
+ }
40
52
  lines.push(`\u{1F552} updated ${relTime(m.updatedAt)} \u00B7 created ${relTime(m.createdAt)}`);
41
53
  const ctx = typeof extra.contextPct === "number" ? ` \u00B7 \u{1F9E0} ctx ${Math.round(extra.contextPct)}%` : "";
42
54
  lines.push(`\u{1F4CA} ${state} \u00B7 \u{1F4DC} history ${humanSize(m.historyBytes)}${ctx}`);
@@ -10,7 +10,7 @@
10
10
  import { type Bot, type Context, InlineKeyboard } from "grammy";
11
11
  import { basename } from "node:path";
12
12
  import type { BotDeps } from "../deps.js";
13
- import { readHistory } from "../../sessions/history.js";
13
+ import { readHistory, readLastCardSummary } from "../../sessions/history.js";
14
14
  import type { SessionMeta } from "../../sessions/types.js";
15
15
  import { refreshMenu } from "../menu/refresh.js";
16
16
  import { showHistory } from "./history.js";
@@ -52,8 +52,19 @@ async function renderSessionPage(ctx: Context, deps: BotDeps, page: number): Pro
52
52
 
53
53
  for (const m of slice) {
54
54
  const contextPct = deps.acp.metadataFor(m.sessionId)?.contextUsagePercentage;
55
- const progress = deps.registry.controller(ctx.chat!.id).progressFor(m.sessionId);
56
- const { text, keyboard } = buildSessionCard(m, { contextPct, selfPid: deps.acp.pid, progress });
55
+ const ctrl = deps.registry.controller(ctx.chat!.id);
56
+ const progress = ctrl.progressFor(m.sessionId);
57
+ // Live runtime → persisted comment → last assistant outcome from history.
58
+ const comment =
59
+ ctrl.commentFor(m.sessionId) ||
60
+ m.comment ||
61
+ readLastCardSummary(deps.store.jsonlPath(m.sessionId));
62
+ const { text, keyboard } = buildSessionCard(m, {
63
+ contextPct,
64
+ selfPid: deps.acp.pid,
65
+ progress,
66
+ comment,
67
+ });
57
68
  await deps.ephemeral.reply(ctx, text, { reply_markup: keyboard });
58
69
  }
59
70
 
@@ -1,7 +1,10 @@
1
1
  /**
2
- * /usage — show account info and the current session's context usage.
2
+ * /usage — Grok CLI live monthly quota + session context + bot-tracked
3
+ * per-account turn stats.
3
4
  */
4
5
  import type { Bot, Context } from "grammy";
6
+ import type { StoredAccount } from "../../app/accounts.js";
7
+ import { formatCliBillingLines } from "../../app/usage.js";
5
8
  import type { BotDeps } from "../deps.js";
6
9
 
7
10
  export async function showUsage(ctx: Context, deps: BotDeps): Promise<void> {
@@ -10,29 +13,128 @@ export async function showUsage(ctx: Context, deps: BotDeps): Promise<void> {
10
13
  const acct = await deps.usage.account();
11
14
  const meta = rt.contextInfo();
12
15
  const ctx100 = meta?.contextUsagePercentage;
13
- const savedCount = deps.accounts.list().length;
16
+ const list = deps.accounts.list();
17
+ const activeId = deps.accounts.activeAccountId();
18
+ const activeMeta = activeId ? deps.accounts.get(activeId) : undefined;
19
+ const { billing, error: billingError } = await deps.usage.cliBilling();
14
20
 
15
- const lines = [
16
- "\u{1F4CA} Usage & account",
17
- acct?.email ? `\u{1F464} ${acct.email}` : "",
18
- acct?.accountType ? `\u{1F511} ${acct.accountType}${acct.region ? ` \u00B7 ${acct.region}` : ""}` : "",
21
+ const lines: string[] = [
22
+ "\u{1F4CA} Usage & accounts",
19
23
  "",
20
- `\u{1F9F5} Session: ${rt.sessionId ? rt.sessionId.slice(0, 8) : "none"}`,
21
- `\u{1F9E9} Model: ${rt.model || "default"}`,
22
- `\u{1F4CA} Context used: ${ctx100 !== undefined ? `${ctx100.toFixed(0)}%` : "\u2014"}`,
23
- `\u{1F501} Turns this session: ${rt.turns}`,
24
- meta?.credits !== undefined ? `\u{1FA99} Credits used: ${meta.credits.toLocaleString("en-US")}` : "",
25
- meta?.effort ? `\u{1F9E0} Effort: ${meta.effort}` : "",
26
- savedCount > 0 ? `\u{1F465} Saved accounts: ${savedCount} \u00B7 /accounts to switch` : "",
24
+ "\u{1F464} Current login",
25
+ acct?.email ? ` ${acct.email}` : " (identity unknown)",
26
+ ];
27
+ if (acct?.accountType) {
28
+ lines.push(` \u{1F511} ${acct.accountType}${acct.region ? ` \u00B7 ${acct.region}` : ""}`);
29
+ }
30
+ if (acct?.teamId) lines.push(` Team: ${acct.teamId.slice(0, 8)}\u2026`);
31
+ if (activeMeta) {
32
+ const uLine = deps.accounts.formatUsageLine(activeMeta);
33
+ lines.push(` Saved as: ${activeMeta.label}`);
34
+ if (uLine) lines.push(` Bot-tracked: ${uLine}`);
35
+ }
36
+
37
+ lines.push("");
38
+ if (billing) {
39
+ lines.push(...formatCliBillingLines(billing));
40
+ } else {
41
+ lines.push(
42
+ "\u{1F4B3} Grok CLI monthly quota",
43
+ ` \u26A0\uFE0F ${billingError || "unavailable"}`,
44
+ " (Requires `grok login` OIDC token — not XAI_API_KEY alone.)",
45
+ );
46
+ }
47
+
48
+ lines.push(
27
49
  "",
28
- "\u2139\uFE0F Full billing/quota lives in the Grok app; grok doesn't expose limits headlessly.",
29
- ].filter(Boolean);
50
+ "\u{1F9F5} This session (ACP)",
51
+ ` Id: ${rt.sessionId ? rt.sessionId.slice(0, 8) : "none"}`,
52
+ ` Model: ${rt.model || "default"}`,
53
+ ` Context used: ${ctx100 !== undefined ? `${ctx100.toFixed(0)}%` : "\u2014"}`,
54
+ ` Turns this session: ${rt.turns}`,
55
+ );
56
+ if (meta?.credits !== undefined) {
57
+ lines.push(` Credits (session report): ${fmtNum(meta.credits)}`);
58
+ }
59
+ if (meta?.effort) lines.push(` Effort: ${meta.effort}`);
60
+ if (meta?.totalTokens !== undefined) {
61
+ lines.push(` Tokens (session report): ${meta.totalTokens.toLocaleString("en-US")}`);
62
+ }
63
+
64
+ if (list.length > 0) {
65
+ lines.push("", "\u{1F465} Saved accounts (bot-tracked turns on this machine)");
66
+ for (const a of list) {
67
+ lines.push(accountUsageBlock(deps, a, a.id === activeId));
68
+ }
69
+ const totals = aggregate(list);
70
+ lines.push(
71
+ "",
72
+ `\u{1F4CA} Bot totals: ${totals.turns} turn${totals.turns === 1 ? "" : "s"}` +
73
+ (totals.credits > 0 ? ` \u00B7 ${fmtNum(totals.credits)} session credits` : "") +
74
+ ` \u00B7 ${totals.withUsage}/${list.length} accounts used`,
75
+ );
76
+ } else {
77
+ lines.push("", "\u{1F465} No saved accounts yet \u2014 /accounts to save & switch.");
78
+ }
79
+
80
+ lines.push(
81
+ "",
82
+ "\u2139\uFE0F Monthly quota is live from Grok CLI (`cli-chat-proxy` billing). Bot-tracked turns are local to this bot.",
83
+ );
84
+
85
+ if (!acct) lines.splice(3, 0, " (account info unavailable \u2014 is grok logged in?)");
30
86
 
31
- if (!acct) lines.splice(1, 0, "(account info unavailable \u2014 is grok logged in?)");
32
87
  await deps.ephemeral.open(ctx);
33
88
  await deps.ephemeral.reply(ctx, lines.join("\n"));
34
89
  }
35
90
 
91
+ function accountUsageBlock(deps: BotDeps, a: StoredAccount, active: boolean): string {
92
+ const mark = a.warning ? "\u26A0\uFE0F" : active ? "\u2705" : "\u{1F464}";
93
+ const u = a.usage;
94
+ const head = `${mark} ${a.label}${active ? " (active)" : ""}`;
95
+ if (!u || (u.turns <= 0 && u.credits <= 0 && !u.lastUsedAt)) {
96
+ return `${head}\n no bot-tracked usage yet`;
97
+ }
98
+ const bits: string[] = [];
99
+ bits.push(`${u.turns || 0} turn${(u.turns || 0) === 1 ? "" : "s"}`);
100
+ if (u.credits > 0) bits.push(`${fmtNum(u.credits)} session credits`);
101
+ if (u.lastTurnCredits !== undefined) bits.push(`last turn ${fmtNum(u.lastTurnCredits)}`);
102
+ if (u.lastContextPct !== undefined) bits.push(`last ctx ${u.lastContextPct.toFixed(0)}%`);
103
+ if (u.lastUsedAt) bits.push(`last ${shortWhen(u.lastUsedAt)}`);
104
+ return `${head}\n ${bits.join(" \u00B7 ")}`;
105
+ }
106
+
107
+ function aggregate(list: StoredAccount[]): { turns: number; credits: number; withUsage: number } {
108
+ let turns = 0;
109
+ let credits = 0;
110
+ let withUsage = 0;
111
+ for (const a of list) {
112
+ const u = a.usage;
113
+ if (!u) continue;
114
+ turns += u.turns || 0;
115
+ credits += u.credits || 0;
116
+ if (u.turns > 0 || u.credits > 0 || u.lastUsedAt) withUsage++;
117
+ }
118
+ return { turns, credits, withUsage };
119
+ }
120
+
121
+ function fmtNum(n: number): string {
122
+ if (!Number.isFinite(n)) return String(n);
123
+ if (Number.isInteger(n)) return n.toLocaleString("en-US");
124
+ return n.toFixed(2);
125
+ }
126
+
127
+ function shortWhen(iso: string): string {
128
+ const t = Date.parse(iso);
129
+ if (!Number.isFinite(t)) return iso.slice(0, 10);
130
+ const sec = Math.max(0, Math.round((Date.now() - t) / 1000));
131
+ if (sec < 60) return "just now";
132
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
133
+ if (sec < 86_400) return `${Math.floor(sec / 3600)}h ago`;
134
+ if (sec < 86_400 * 14) return `${Math.floor(sec / 86_400)}d ago`;
135
+ return new Date(t).toISOString().slice(0, 10);
136
+ }
137
+
36
138
  export function registerUsage(bot: Bot, deps: BotDeps): void {
37
139
  bot.command("usage", (ctx) => showUsage(ctx, deps));
38
140
  }
@@ -2,8 +2,9 @@
2
2
  * Menu surfaces:
3
3
  * - a tiny PERSISTENT bar (☰ Menu · 🧭 Running · ⏹ Stop) — minimal footprint;
4
4
  * - a full, organized INLINE menu opened on demand (and hideable).
5
- * Live state (project/agent/model/reasoning/context) lives in the pinned panel,
6
- * so the bar stays clean.
5
+ * Live state (project/model/reasoning/context) lives in the pinned panel,
6
+ * so the bar stays clean. (Agent picker removed — Grok has no useful headless
7
+ * agent switch; plan mode is entered automatically when the agent judges the task complex.)
7
8
  */
8
9
  import { InlineKeyboard, Keyboard } from "grammy";
9
10
 
@@ -18,7 +19,7 @@ export function compactKeyboard(): Keyboard {
18
19
  }
19
20
 
20
21
  /** The full, grouped inline menu (opened via ☰ Menu or /menu). */
21
- export function mainMenuInline(state: { agent: string; model: string; reasoning: string }): InlineKeyboard {
22
+ export function mainMenuInline(state: { model: string; reasoning: string }): InlineKeyboard {
22
23
  const t = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + "\u2026" : s);
23
24
  return new InlineKeyboard()
24
25
  .text("\u{1F4C1} Project", "m:project")
@@ -27,7 +28,7 @@ export function mainMenuInline(state: { agent: string; model: string; reasoning:
27
28
  .text("\u{1F9ED} Running", "m:running")
28
29
  .text("\u{1F5C2} Sessions", "m:sessions")
29
30
  .row()
30
- .text(`\u{1F916} Agent \u00B7 ${t(state.agent, 24)}`, "m:agent")
31
+ .text("\u{1F4E5} Import session", "m:import")
31
32
  .row()
32
33
  .text(`\u{1F9E9} Model \u00B7 ${t(state.model, 24)}`, "m:model")
33
34
  .row()
@@ -49,11 +49,15 @@ export class StatusPanel {
49
49
  const SEP = " | "; // pipe delimiter between inline fields
50
50
  const lines: string[] = [];
51
51
 
52
- // 1) Progress first only while a turn is live (cleared when it ends), so
53
- // the collapsed pin preview shows how far along the current task is.
52
+ // 1) Active plan board first (always above the progress bar) so the user
53
+ // always sees done / in-progress / pending steps while a plan is live.
54
+ const plan = rt.planBoard;
55
+ if (plan) lines.push(plan);
56
+
57
+ // 2) Progress — only while a turn is live (cleared when it ends).
54
58
  if (progress !== undefined) lines.push(`\u{1F4C8} ${progressBar(progress)}`);
55
59
 
56
- // 2) Activity: state + only the counters that currently apply.
60
+ // 3) Activity: state + only the counters that currently apply.
57
61
  const activity: string[] = [rt.isBusy ? "\u23F3 Working" : "\u2705 Idle"];
58
62
  if (rt.queueLength > 0) activity.push(`\u{1F4E5} ${rt.queueLength} queued`);
59
63
  if (running > 1) activity.push(`\u{1F9ED} ${running} sessions`);
@@ -61,13 +65,22 @@ export class StatusPanel {
61
65
  if (subagents) activity.push(`\u{1F465} ${subagents}`);
62
66
  lines.push(activity.join(SEP));
63
67
 
64
- // 3) Where: project | session | context usage.
68
+ // 3b) What is happening now / last summary (same source as Running cards).
69
+ // Prefer plan one-liner over a redundant tool-step when a plan is active.
70
+ const comment = rt.planSummary && rt.isBusy ? undefined : rt.cardComment;
71
+ if (comment) {
72
+ const icon = rt.isBusy ? "\u23F3" : "\u{1F4AC}";
73
+ const shown = comment.length > 120 ? `${comment.slice(0, 119)}\u2026` : comment;
74
+ lines.push(`${icon} ${shown}`);
75
+ }
76
+
77
+ // 4) Where: project | session | context usage.
65
78
  const loc = [`\u{1F4C1} ${project}`, `\u{1F9F5} ${session}`];
66
79
  if (ctxPct !== undefined) loc.push(`\u{1F4CA} ${ctxPct.toFixed(0)}% context`);
67
80
  lines.push(loc.join(SEP));
68
81
 
69
- // 4) How: agent | reasoning | model.
70
- lines.push([`\u{1F916} ${s.agent || "default"}`, `\u{1F9E0} ${reasoningLabel(s.reasoning)}`, `\u{1F9E9} ${s.model || "default"}`].join(SEP));
82
+ // 5) How: reasoning | model (agent picker removed — plan mode is automatic).
83
+ lines.push([`\u{1F9E0} ${reasoningLabel(s.reasoning)}`, `\u{1F9E9} ${s.model || "default"}`].join(SEP));
71
84
 
72
85
  return lines.join("\n");
73
86
  }
@@ -66,6 +66,9 @@ export function mergeInputs(inputs: PromptInput[]): PromptInput {
66
66
  const quotes = inputs
67
67
  .map((i) => i.quotedText?.trim())
68
68
  .filter((q): q is string => !!q);
69
+ // Preserve meta flags: auto-suggestion batches / self-recheck must not re-arm
70
+ // another recheck after merge (dropping this caused infinite recheck loops).
71
+ const skipSelfRecheck = inputs.some((i) => i.skipSelfRecheck);
69
72
  return {
70
73
  text: inputs
71
74
  .map((i) => i.text)
@@ -75,6 +78,7 @@ export function mergeInputs(inputs: PromptInput[]): PromptInput {
75
78
  resourceLinks: inputs.flatMap((i) => i.resourceLinks ?? []),
76
79
  replyTo: inputs.find((i) => i.replyTo !== undefined)?.replyTo,
77
80
  quotedText: quotes.length > 0 ? [...new Set(quotes)].join("\n\n---\n\n") : undefined,
81
+ skipSelfRecheck: skipSelfRecheck || undefined,
78
82
  };
79
83
  }
80
84
 
@@ -180,7 +180,7 @@ export class ReauthController {
180
180
  }
181
181
  s.phase = "restarting";
182
182
  await this.render(s);
183
- await this.grok.start();
183
+ await this.grok.start(true);
184
184
  agentDown = false;
185
185
  s.accountLabel = accountLabel(await this.getAccount?.().catch(() => undefined));
186
186
  s.phase = "done";
@@ -191,7 +191,7 @@ export class ReauthController {
191
191
  } finally {
192
192
  s.abort = undefined;
193
193
  this.stopAnim(s);
194
- if (agentDown) await this.grok.start().catch((e) => log.warn("post-reauth restart failed:", (e as Error).message));
194
+ if (agentDown) await this.grok.start(true).catch((e) => log.warn("post-reauth restart failed:", (e as Error).message));
195
195
  await this.render(s);
196
196
  }
197
197
  }
@@ -33,3 +33,14 @@ export function buildPriming(transcript: string): string {
33
33
  "=== END TRANSCRIPT ===",
34
34
  ].join("\n");
35
35
  }
36
+
37
+ /**
38
+ * First-turn prompt body used after a foreign-session import. The heavy
39
+ * transcript lives in {@link SessionRuntime}'s primingContext; this is the
40
+ * short user message that flushes priming into the live Grok session.
41
+ */
42
+ export const IMPORT_CONFIRM_PROMPT =
43
+ "Session import complete. Confirm you have the full imported context " +
44
+ "(read the transcript file if anything was truncated inline). " +
45
+ "Reply with a one-line ready confirmation: project path + one-sentence " +
46
+ "summary of the task so far. Do not continue work until I send the next message.";