switchroom 0.16.28 → 0.16.38
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/agent-scheduler/index.js +13 -2
- package/dist/auth-broker/index.js +13 -2
- package/dist/cli/notion-write-pretool.mjs +13 -2
- package/dist/cli/switchroom.js +60648 -59800
- package/dist/host-control/main.js +730 -18
- package/dist/vault/approvals/kernel-server.js +51 -37
- package/dist/vault/broker/server.js +72 -58
- package/package.json +2 -1
- package/profiles/_shared/agent-self-service.md.hbs +7 -2
- package/telegram-plugin/auth-snapshot-format.ts +173 -67
- package/telegram-plugin/auto-fallback-fleet.ts +3 -6
- package/telegram-plugin/card-format.ts +52 -2
- package/telegram-plugin/credits-watch.ts +4 -7
- package/telegram-plugin/dist/gateway/gateway.js +2855 -1034
- package/telegram-plugin/dist/server.js +0 -1
- package/telegram-plugin/format.ts +264 -7
- package/telegram-plugin/gateway/approval-card.ts +7 -14
- package/telegram-plugin/gateway/approvals-commands.ts +6 -9
- package/telegram-plugin/gateway/auth-command.ts +35 -38
- package/telegram-plugin/gateway/boot-card.ts +7 -2
- package/telegram-plugin/gateway/chat-id-fallback.ts +46 -0
- package/telegram-plugin/gateway/config-approval-handler.ts +6 -9
- package/telegram-plugin/gateway/diff-preview-card.ts +3 -6
- package/telegram-plugin/gateway/gateway.ts +1293 -97
- package/telegram-plugin/gateway/ipc-protocol.ts +66 -2
- package/telegram-plugin/gateway/ipc-server.ts +91 -1
- package/telegram-plugin/gateway/linear-activity.ts +2 -5
- package/telegram-plugin/gateway/obligation-turn-end.ts +27 -0
- package/telegram-plugin/gateway/permission-card-store.ts +104 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -6
- package/telegram-plugin/gateway/status-pin-store.ts +302 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +30 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +12 -1
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +35 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +61 -0
- package/telegram-plugin/history.ts +17 -7
- package/telegram-plugin/idle-footer.ts +2 -2
- package/telegram-plugin/issues-card.ts +12 -7
- package/telegram-plugin/model-unavailable.ts +3 -6
- package/telegram-plugin/operator-events.ts +4 -6
- package/telegram-plugin/quota-check.ts +2 -2
- package/telegram-plugin/quota-watch.ts +7 -10
- package/telegram-plugin/server.ts +3 -1
- package/telegram-plugin/status-pin-driver.ts +102 -0
- package/telegram-plugin/status-pin.ts +76 -0
- package/telegram-plugin/stream-reply-handler.ts +14 -1
- package/telegram-plugin/subagent-watcher.ts +6 -3
- package/telegram-plugin/tests/auth-command-format2.test.ts +6 -2
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +18 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +204 -45
- package/telegram-plugin/tests/card-format.test.ts +51 -0
- package/telegram-plugin/tests/chat-id-fallback.test.ts +74 -0
- package/telegram-plugin/tests/codespan-escaping-golden.test.ts +166 -0
- package/telegram-plugin/tests/credits-watch.test.ts +18 -0
- package/telegram-plugin/tests/history.test.ts +38 -2
- package/telegram-plugin/tests/idle-footer.test.ts +53 -9
- package/telegram-plugin/tests/ipc-server-validate-rollout-status.test.ts +66 -0
- package/telegram-plugin/tests/issues-card.test.ts +24 -0
- package/telegram-plugin/tests/obligation-turn-end.test.ts +76 -0
- package/telegram-plugin/tests/paragraph-normalizer.test.ts +200 -1
- package/telegram-plugin/tests/permission-card-single-edit.test.ts +66 -0
- package/telegram-plugin/tests/permission-card-store.test.ts +83 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +37 -6
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +23 -6
- package/telegram-plugin/tests/quota-check.test.ts +9 -2
- package/telegram-plugin/tests/quota-watch.test.ts +7 -7
- package/telegram-plugin/tests/rollout-status-wiring.test.ts +81 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +294 -0
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +210 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +450 -0
- package/telegram-plugin/tests/status-pin.test.ts +202 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +39 -0
- package/telegram-plugin/tests/telegram-format.test.ts +88 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +82 -24
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +39 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +12 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +54 -0
- package/telegram-plugin/tests/vault-request-access-card.test.ts +94 -0
- package/telegram-plugin/tests/voice-normalize-text.test.ts +256 -0
- package/telegram-plugin/tests/voice-ondemand.test.ts +202 -0
- package/telegram-plugin/tests/voice-out-one-send.test.ts +153 -0
- package/telegram-plugin/tests/voice-synthesize-sidecar.test.ts +352 -0
- package/telegram-plugin/tests/voice-transcribe-sidecar.test.ts +332 -0
- package/telegram-plugin/tests/voice-transcribe.test.ts +188 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +28 -10
- package/telegram-plugin/tool-activity-summary.ts +11 -8
- package/telegram-plugin/uat/driver.ts +8 -1
- package/telegram-plugin/voice-normalize-text.ts +340 -0
- package/telegram-plugin/voice-ondemand.ts +138 -0
- package/telegram-plugin/voice-synthesize-sidecar.ts +259 -0
- package/telegram-plugin/voice-synthesize.ts +128 -0
- package/telegram-plugin/voice-transcribe-sidecar.ts +176 -0
- package/telegram-plugin/worker-activity-feed.ts +13 -4
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
fmtPct,
|
|
52
52
|
} from "./auth-snapshot-format.js";
|
|
53
53
|
import type { QuotaUtilization } from "./quota-check.js";
|
|
54
|
+
import { escapeMarkdown, codeSpanSafe } from "./card-format.js";
|
|
54
55
|
|
|
55
56
|
const STATE_FILE = "quota-watch.json";
|
|
56
57
|
|
|
@@ -511,7 +512,7 @@ function buildFleetRecoveredMessage(
|
|
|
511
512
|
accounts: Array<{ label: string; exhausted: boolean }>,
|
|
512
513
|
): string {
|
|
513
514
|
const healthy = accounts.filter((a) => !a.exhausted).map((a) => a.label);
|
|
514
|
-
const which = healthy.length > 0 ? ` (\`${
|
|
515
|
+
const which = healthy.length > 0 ? ` (\`${codeSpanSafe(healthy[0]!)}\`)` : "";
|
|
515
516
|
return [
|
|
516
517
|
`🟢 **Fleet recovered** — at least one account is healthy again${which}.`,
|
|
517
518
|
``,
|
|
@@ -521,7 +522,7 @@ function buildFleetRecoveredMessage(
|
|
|
521
522
|
|
|
522
523
|
// ─── Message builders ─────────────────────────────────────────────────────────
|
|
523
524
|
|
|
524
|
-
function buildThrottlingMessage(agentName: string, snap: AccountSnapshot): string {
|
|
525
|
+
export function buildThrottlingMessage(agentName: string, snap: AccountSnapshot): string {
|
|
525
526
|
const q = snap.quota!; // classifyHealth returned throttling, so quota is non-null
|
|
526
527
|
const fiveStr = fmtPct(q.fiveHourUtilizationPct);
|
|
527
528
|
const sevenStr = fmtPct(q.sevenDayUtilizationPct);
|
|
@@ -535,14 +536,14 @@ function buildThrottlingMessage(agentName: string, snap: AccountSnapshot): strin
|
|
|
535
536
|
|
|
536
537
|
const activeNote = snap.isActive
|
|
537
538
|
? ""
|
|
538
|
-
: `\nThis is a non-active account. Consider \`/auth use ${
|
|
539
|
+
: `\nThis is a non-active account. Consider \`/auth use ${codeSpanSafe(snap.label)}\` to switch, or keep it as a fallback reserve.`;
|
|
539
540
|
|
|
540
541
|
const altNote = snap.isActive
|
|
541
542
|
? `\nConsider \`/auth use <other-account>\` if you have a healthier account, or wait for the ${winLabel} window to refill${resetStr}.`
|
|
542
543
|
: "";
|
|
543
544
|
|
|
544
545
|
return [
|
|
545
|
-
`🟡 **Quota approaching limit** — \`${
|
|
546
|
+
`🟡 **Quota approaching limit** — \`${codeSpanSafe(snap.label)}\``,
|
|
546
547
|
``,
|
|
547
548
|
`${fiveStr} of 5h · ${sevenStr} of 7d`,
|
|
548
549
|
`Binding window: ${winLabel}${resetStr}`,
|
|
@@ -556,14 +557,14 @@ function buildThrottlingMessage(agentName: string, snap: AccountSnapshot): strin
|
|
|
556
557
|
.trim();
|
|
557
558
|
}
|
|
558
559
|
|
|
559
|
-
function buildRecoveryMessage(agentName: string, snap: AccountSnapshot): string {
|
|
560
|
+
export function buildRecoveryMessage(agentName: string, snap: AccountSnapshot): string {
|
|
560
561
|
const q = snap.quota;
|
|
561
562
|
const utilLine = q
|
|
562
563
|
? `Current: ${fmtPct(q.fiveHourUtilizationPct)} of 5h · ${fmtPct(q.sevenDayUtilizationPct)} of 7d`
|
|
563
564
|
: "Current quota data unavailable.";
|
|
564
565
|
|
|
565
566
|
return [
|
|
566
|
-
`🟢 **Quota back in healthy range** — \`${
|
|
567
|
+
`🟢 **Quota back in healthy range** — \`${codeSpanSafe(snap.label)}\``,
|
|
567
568
|
``,
|
|
568
569
|
utilLine,
|
|
569
570
|
``,
|
|
@@ -571,10 +572,6 @@ function buildRecoveryMessage(agentName: string, snap: AccountSnapshot): string
|
|
|
571
572
|
].join("\n");
|
|
572
573
|
}
|
|
573
574
|
|
|
574
|
-
function escapeHtml(s: string): string {
|
|
575
|
-
return s.replace(/([\\`*_~=\[\]|])/g, "\\$1");
|
|
576
|
-
}
|
|
577
|
-
|
|
578
575
|
// ─── State persistence ────────────────────────────────────────────────────────
|
|
579
576
|
|
|
580
577
|
export function loadQuotaWatchState(stateDir: string): QuotaWatchState {
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
* - Robust error handling: 429 retry, thread-not-found fallback, network retry
|
|
31
31
|
* - SQLite history buffer (history.ts) for cross-restart recovery
|
|
32
32
|
* - Vault-grant inline-keyboard wizard
|
|
33
|
-
* -
|
|
33
|
+
* - Live in-conversation status feed with sub-agent visibility (the pinned
|
|
34
|
+
* progress card was retired in #1122; the status message is now a regular
|
|
35
|
+
* in-chat message, silently pinned while in-flight — status-pin.ts)
|
|
34
36
|
*
|
|
35
37
|
* State (access.json / pairing / allowlists) lives at
|
|
36
38
|
* ~/.claude/channels/telegram/ — managed by the /telegram:access skill.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status-pin driver — executes a PinAction against a Telegram Bot API.
|
|
3
|
+
*
|
|
4
|
+
* The pure decision lives in `status-pin.ts` (decidePinAction). This
|
|
5
|
+
* module is the side-effecting half: takes the previously-claimed state
|
|
6
|
+
* and the desired state, computes ONE action, executes it, and returns
|
|
7
|
+
* the next state. The state itself stays in the caller (the gateway holds
|
|
8
|
+
* a `Map<pinKey, PinState>` and re-passes the entry on every call).
|
|
9
|
+
*
|
|
10
|
+
* Copied from `slot-banner-driver.ts`. The load-bearing contract (see
|
|
11
|
+
* slot-banner-driver.ts:100-110):
|
|
12
|
+
*
|
|
13
|
+
* - Pins are SILENT (`disable_notification: true`) — a status pin must
|
|
14
|
+
* never buzz the device.
|
|
15
|
+
* - We pin an EXISTING message (the already-rendered per-turn status /
|
|
16
|
+
* `🛠 Worker` message). We NEVER send a new message to pin. This is
|
|
17
|
+
* the boundary that keeps us inside the one sanctioned exception on
|
|
18
|
+
* `chat-is-the-single-source-of-truth`.
|
|
19
|
+
* - On UNPIN we DROP THE CLAIM (clear state) EVEN IF `unpinChatMessage`
|
|
20
|
+
* throws. This is the whole point: pin state must never get stuck.
|
|
21
|
+
* The message may have been unpinned out-of-band (operator, or a
|
|
22
|
+
* crash) — re-claiming it would be more confusing than surfacing it
|
|
23
|
+
* again later, and a stuck claim would leave a permanent pin.
|
|
24
|
+
* - API failures are reported via `onError` but never throw; the caller
|
|
25
|
+
* decides logging cadence.
|
|
26
|
+
*
|
|
27
|
+
* See `reference/invariants.md` § `chat-is-the-single-source-of-truth`
|
|
28
|
+
* (the sanctioned silent-pin exception) and the `know-what-my-agent-is-doing`
|
|
29
|
+
* job spec.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { PinState, DesiredPin } from './status-pin.js'
|
|
33
|
+
import { decidePinAction } from './status-pin.js'
|
|
34
|
+
|
|
35
|
+
/** Minimal subset of grammy's `bot.api` the pin driver depends on.
|
|
36
|
+
* Lets tests swap in a fake without dragging in the full Bot type. */
|
|
37
|
+
export interface PinBotApi {
|
|
38
|
+
pinChatMessage(
|
|
39
|
+
chat_id: string | number,
|
|
40
|
+
message_id: number,
|
|
41
|
+
opts?: Record<string, unknown>,
|
|
42
|
+
): Promise<unknown>
|
|
43
|
+
unpinChatMessage(
|
|
44
|
+
chat_id: string | number,
|
|
45
|
+
message_id: number,
|
|
46
|
+
): Promise<unknown>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ReconcilePinArgs {
|
|
50
|
+
api: PinBotApi
|
|
51
|
+
chatId: string
|
|
52
|
+
/** State the caller is holding from the last reconcile. `null` on first. */
|
|
53
|
+
prevState: PinState | null
|
|
54
|
+
/** What the caller wants pinned for this key right now. */
|
|
55
|
+
desired: DesiredPin
|
|
56
|
+
/** Optional API-failure observer. Default: silent. */
|
|
57
|
+
onError?: (phase: 'pin' | 'unpin', err: unknown) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute the next pin-state transition for one key. Returns the new
|
|
62
|
+
* `PinState` (or `null` when unpinned / nothing pinned). Always resolves;
|
|
63
|
+
* never throws — API errors route through `onError`.
|
|
64
|
+
*
|
|
65
|
+
* - `pin` : pins the existing message SILENTLY; on failure the claim is
|
|
66
|
+
* NOT taken (returns prevState) so the next reconcile retries
|
|
67
|
+
* rather than tracking a message it never pinned.
|
|
68
|
+
* - `unpin`: unpins best-effort and returns `null` — the claim is dropped
|
|
69
|
+
* EVEN IF the unpin throws (never leave state stuck pinned).
|
|
70
|
+
* - `noop` : returns prevState unchanged.
|
|
71
|
+
*/
|
|
72
|
+
export async function reconcilePin(
|
|
73
|
+
args: ReconcilePinArgs,
|
|
74
|
+
): Promise<PinState | null> {
|
|
75
|
+
const action = decidePinAction(args.prevState, args.desired)
|
|
76
|
+
|
|
77
|
+
if (action.kind === 'noop') return args.prevState
|
|
78
|
+
|
|
79
|
+
if (action.kind === 'unpin') {
|
|
80
|
+
try {
|
|
81
|
+
await args.api.unpinChatMessage(args.chatId, action.messageId)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
args.onError?.('unpin', err)
|
|
84
|
+
}
|
|
85
|
+
// Drop the claim regardless of the unpin outcome. A stuck claim would
|
|
86
|
+
// leave a permanent pin on a crash / out-of-band unpin — the exact
|
|
87
|
+
// failure this driver exists to prevent (see slot-banner-driver.ts).
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// action.kind === 'pin' — pin an EXISTING message, silently.
|
|
92
|
+
try {
|
|
93
|
+
await args.api.pinChatMessage(args.chatId, action.messageId, {
|
|
94
|
+
disable_notification: true,
|
|
95
|
+
})
|
|
96
|
+
} catch (err) {
|
|
97
|
+
args.onError?.('pin', err)
|
|
98
|
+
// Don't claim a message we failed to pin — the next reconcile retries.
|
|
99
|
+
return args.prevState
|
|
100
|
+
}
|
|
101
|
+
return { messageId: action.messageId }
|
|
102
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status-pin — pure decision logic.
|
|
3
|
+
*
|
|
4
|
+
* Keeps an already-rendered status message pinned while its work is
|
|
5
|
+
* in-flight, and unpins it the moment the work completes. This is the
|
|
6
|
+
* ONE sanctioned pin under the `chat-is-the-single-source-of-truth`
|
|
7
|
+
* invariant (see `reference/invariants.md` §
|
|
8
|
+
* `chat-is-the-single-source-of-truth` and the
|
|
9
|
+
* `know-what-my-agent-is-doing` job spec): it re-surfaces a message the
|
|
10
|
+
* conversation ALREADY owns — the per-turn activity/status message and
|
|
11
|
+
* the `🛠 Worker` background-worker message — and never renders a new
|
|
12
|
+
* parallel surface.
|
|
13
|
+
*
|
|
14
|
+
* Why a pin at all: fast answers, more background workers, and much
|
|
15
|
+
* longer turns mean the conversation moves on quickly in the feed and
|
|
16
|
+
* the user loses sight of in-flight work — work now routinely outlives
|
|
17
|
+
* the visible window. Pinning the message the feed already holds keeps
|
|
18
|
+
* it in view; it carries no new content.
|
|
19
|
+
*
|
|
20
|
+
* This module is dependency-free so it's provable in isolation; the
|
|
21
|
+
* gateway (via `status-pin-driver.ts`) translates a `PinAction` into
|
|
22
|
+
* actual Telegram API calls. The design mirrors `slot-banner.ts` +
|
|
23
|
+
* `slot-banner-driver.ts` — one pure decision, one side-effecting
|
|
24
|
+
* reconcile, the claim dropped on unpin even when the API throws.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The message the framework is currently claiming as pinned for a key. */
|
|
28
|
+
export type PinState = {
|
|
29
|
+
/** Telegram message_id we pinned. */
|
|
30
|
+
messageId: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What the caller wants pinned for a key right now. */
|
|
34
|
+
export type DesiredPin =
|
|
35
|
+
/** Work is in-flight; this already-rendered message should be pinned. */
|
|
36
|
+
| { pinned: true; messageId: number }
|
|
37
|
+
/** Work is done (or never opened a message); nothing should be pinned. */
|
|
38
|
+
| { pinned: false }
|
|
39
|
+
|
|
40
|
+
export type PinAction =
|
|
41
|
+
| { kind: 'noop'; reason: string }
|
|
42
|
+
/** Pin an EXISTING message. Caller pins, then records the message_id
|
|
43
|
+
* back into PinState. No new message is sent — this pins a message the
|
|
44
|
+
* chat already rendered. */
|
|
45
|
+
| { kind: 'pin'; messageId: number }
|
|
46
|
+
/** Unpin + forget. Caller unpins (best-effort) and clears state EVEN IF
|
|
47
|
+
* the unpin call throws. */
|
|
48
|
+
| { kind: 'unpin'; messageId: number }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decide the single pin action for a key given the previously-claimed
|
|
52
|
+
* state and what the caller now wants.
|
|
53
|
+
*
|
|
54
|
+
* Exactly one action per (prev, desired) — the whole point is that pin
|
|
55
|
+
* state can never get stuck: an in-flight → done transition always yields
|
|
56
|
+
* an `unpin`, and the driver drops the claim on unpin regardless of API
|
|
57
|
+
* outcome.
|
|
58
|
+
*/
|
|
59
|
+
export function decidePinAction(
|
|
60
|
+
prev: PinState | null,
|
|
61
|
+
desired: DesiredPin,
|
|
62
|
+
): PinAction {
|
|
63
|
+
if (!desired.pinned) {
|
|
64
|
+
if (prev) return { kind: 'unpin', messageId: prev.messageId }
|
|
65
|
+
return { kind: 'noop', reason: 'nothing pinned, nothing wanted' }
|
|
66
|
+
}
|
|
67
|
+
// desired.pinned === true
|
|
68
|
+
if (prev == null) return { kind: 'pin', messageId: desired.messageId }
|
|
69
|
+
if (prev.messageId === desired.messageId) {
|
|
70
|
+
return { kind: 'noop', reason: 'already pinned this message' }
|
|
71
|
+
}
|
|
72
|
+
// The message we want pinned changed (the feed re-posted after a stale
|
|
73
|
+
// edit dropped its id). Unpin the old claim; a subsequent desired-pin
|
|
74
|
+
// re-pins the new one on the next reconcile.
|
|
75
|
+
return { kind: 'unpin', messageId: prev.messageId }
|
|
76
|
+
}
|
|
@@ -158,6 +158,14 @@ export interface StreamReplyDeps {
|
|
|
158
158
|
* the raw (repaired) text is sent unchanged.
|
|
159
159
|
*/
|
|
160
160
|
normalizeParagraphBreaks?: (text: string) => string
|
|
161
|
+
/**
|
|
162
|
+
* Insert a visible blank-line spacer into each prose `\n\n` gap so the rich
|
|
163
|
+
* GFM renderer shows a real empty line between paragraphs (the rich engine
|
|
164
|
+
* otherwise renders `\n\n` tight — the post-#2669 paragraph-spacing
|
|
165
|
+
* regression). Applied only on the rich path (never on `format:'text'`).
|
|
166
|
+
* Optional for backward compat; omitted → no spacers added.
|
|
167
|
+
*/
|
|
168
|
+
addParagraphSpacers?: (text: string) => string
|
|
161
169
|
/** Validates the chat id against the access list. Throws on deny. */
|
|
162
170
|
assertAllowedChat: (chatId: string) => void
|
|
163
171
|
/** Resolves the effective thread id (explicit, last-inbound, or undefined). */
|
|
@@ -337,7 +345,12 @@ export async function handleStreamReply(
|
|
|
337
345
|
// markdown→HTML / MarkdownV2 rendering happens here anymore — the raw
|
|
338
346
|
// text IS the wire payload.
|
|
339
347
|
const literalText = format === 'text'
|
|
340
|
-
|
|
348
|
+
// Paragraph-spacing fix (rich-message regression after #2669): inject a
|
|
349
|
+
// visible blank-line spacer into prose `\n\n` gaps on the rich path so
|
|
350
|
+
// multi-paragraph answers don't render jammed together. The literal
|
|
351
|
+
// (`format:'text'`) path must stay byte-exact, so it is left untouched.
|
|
352
|
+
let effectiveText: string =
|
|
353
|
+
!literalText && deps.addParagraphSpacers ? deps.addParagraphSpacers(rawText) : rawText
|
|
341
354
|
|
|
342
355
|
// Inline status-accent header (issue #320 fallback). Prepended so it
|
|
343
356
|
// leads the body. Since stream_reply callers pass the full text snapshot
|
|
@@ -13,9 +13,12 @@
|
|
|
13
13
|
* does not yet exist (Phase 2 Pre hook hasn't fired), the update is a no-op
|
|
14
14
|
* and the event is logged — no INSERT here, identity belongs to Phase 2.
|
|
15
15
|
*
|
|
16
|
-
* Sub-agent state is surfaced to the user
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Sub-agent state is surfaced to the user in the conversation itself — a
|
|
17
|
+
* background worker via its in-chat `🛠 Worker` message (worker-activity-feed.ts),
|
|
18
|
+
* a foreground sub-agent nested in the parent turn's activity message. The
|
|
19
|
+
* pinned progress card was retired in #1122; the `🛠 Worker` message is now
|
|
20
|
+
* silently pinned while the worker runs (status-pin.ts), not a bespoke card.
|
|
21
|
+
* See issue #142.
|
|
19
22
|
*
|
|
20
23
|
* Architecture notes:
|
|
21
24
|
* - Option B from the spec: filesystem-driven, no IPC contract.
|
|
@@ -93,8 +93,12 @@ describe('renderShowText — Format 2 vs legacy', () => {
|
|
|
93
93
|
});
|
|
94
94
|
expect(out).toContain('🔋 **Auth — fleet status**');
|
|
95
95
|
expect(out).toContain('Recommendation:');
|
|
96
|
-
|
|
97
|
-
expect(out).toContain('
|
|
96
|
+
// GFM table card (#2700): State emoji per row, not group headers.
|
|
97
|
+
expect(out).toContain('| State | Account | 5h | 7d | Status |');
|
|
98
|
+
expect(out).toContain('| 🔴 |'); // the blocked account's State cell
|
|
99
|
+
expect(out).toContain('| 🟢 |'); // a healthy account's State cell
|
|
100
|
+
expect(out).not.toContain('**BLOCKED**');
|
|
101
|
+
expect(out).not.toContain('**HEALTHY**');
|
|
98
102
|
// Legacy ASCII column headers should be absent
|
|
99
103
|
expect(out).not.toContain('ACCOUNT STATUS');
|
|
100
104
|
});
|
|
@@ -140,6 +140,24 @@ describe('parseAuthCommand — new verbs', () => {
|
|
|
140
140
|
expect((p as { reason?: string }).reason).toMatch(/confirm/i)
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
+
// Boundary-escaping (#2695 escaper de-dup): the help/reason strings now
|
|
144
|
+
// render dynamic user input via `codeSpanSafe` — the reason interpolates the
|
|
145
|
+
// value INSIDE a `code span`, where backslash escaping is wrong (#2695). A
|
|
146
|
+
// metacharacter-laden verb must come back LITERAL (no stray backslashes).
|
|
147
|
+
it('renders a metacharacter-laden unknown verb literally in the code span', () => {
|
|
148
|
+
const p = parseAuthCommand('/auth a_b*c')
|
|
149
|
+
expect(p?.kind).toBe('help')
|
|
150
|
+
expect((p as { reason?: string }).reason).toContain('`a_b*c`')
|
|
151
|
+
expect((p as { reason?: string }).reason).not.toContain('a\\_b\\*c')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('renders a metacharacter-laden rm modifier literally in the code span', () => {
|
|
155
|
+
const p = parseAuthCommand('/auth rm spare x_y*z')
|
|
156
|
+
expect(p?.kind).toBe('help')
|
|
157
|
+
expect((p as { reason?: string }).reason).toContain('`x_y*z`')
|
|
158
|
+
expect((p as { reason?: string }).reason).not.toContain('x\\_y\\*z')
|
|
159
|
+
})
|
|
160
|
+
|
|
143
161
|
it('rejects /auth rm with no label', () => {
|
|
144
162
|
const p = parseAuthCommand('/auth rm')
|
|
145
163
|
expect(p?.kind).toBe('help')
|