Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- Optional per-message timestamps in the interactive TUI, off by default and togglable from settings ([#1206](https://github.com/code-yeongyu/senpi/pull/1206)).

### Fixed

- The shared interactive host no longer surfaces raw `Error: Client not started` when its socket drops: the RPC client classifies transport loss with a typed error, the TUI runtime makes bounded reconnect attempts and then genuinely falls back to the local session with a single warning, and in-flight actions resolve as cancellations instead of error toasts ([#1220](https://github.com/code-yeongyu/senpi/pull/1220)).
Expand Down
10 changes: 5 additions & 5 deletions packages/coding-agent/src/changes.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# changes

## 2026-08-30 - Export the RPC transport-gone classifier
## 2026-08-30 - Export and normalize the RPC transport-gone classifier

### What changed

- `index.ts` and `modes/index.ts` re-export `RpcTransportGoneError` and `isTransportGoneError` beside `RpcClient` so embedders can classify shared-host transport loss.
- `packages/coding-agent/src/index.ts` and `packages/coding-agent/src/modes/index.ts` re-export `RpcTransportGoneError` and `isTransportGoneError` beside `RpcClient` so embedders can classify shared-host transport loss, with their mixed type/value export lists normalized to the repository's Biome order during integration.

### Why

- The reconnect-or-fallback orchestration rejects sends with the typed error; consumers of the public client surface need the classifier to distinguish transport loss from real failures.
- `packages/coding-agent/src/index.ts` and `packages/coding-agent/src/modes/index.ts` expose the classifier because reconnect-or-fallback orchestration rejects sends with the typed error; consumers of the public client surface need to distinguish transport loss from real failures, and the canonical gate rewrites both touched export lists into deterministic order.

### Why an extension could not handle it

- Package export surfaces are compile-time module structure; extensions cannot add public exports.
- `packages/coding-agent/src/index.ts` and `packages/coding-agent/src/modes/index.ts` are compile-time package export surfaces; extensions cannot add public exports or control their source ordering.

### Expected merge conflict zones

- LOW: export lists in `index.ts` and `modes/index.ts`.
- LOW: export lists in `packages/coding-agent/src/index.ts` and `packages/coding-agent/src/modes/index.ts`.

## 2026-08-30 - Dispatch the internal RPC host route through wrapper-injected argv

Expand Down
19 changes: 18 additions & 1 deletion packages/coding-agent/src/core/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# changes

## 2026-08-30 - Persist optional assistant message timestamps

### What changed

- `packages/coding-agent/src/core/settings-manager.ts`: adds the default-off `showMessageTimestamps` setting, getter, and setter.

### Why

- Operators can opt into compact local arrival times for assistant messages, and the choice survives later sessions.

### Why an extension could not handle it

- The built-in settings manager owns persisted interactive preferences and their reload semantics before extension UI hooks run.

### Expected merge conflict zones

- `packages/coding-agent/src/core/settings-manager.ts`: the settings schema, public getter set, and persisted setter block.

## 2026-08-31 - Shared session host is OFF by default (opt-in)

- Interactive sessions no longer join the shared RPC host implicitly. `main.ts` now gates
Expand Down Expand Up @@ -62,7 +80,6 @@
so the new-behavior test exercises the lane the contract actually covers instead of the
default print lane.


## 2026-08-30 - Experimental workflow eval-only policy

### What changed
Expand Down
11 changes: 11 additions & 0 deletions packages/coding-agent/src/core/settings-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export interface Settings {
branchSummary?: BranchSummarySettings;
retry?: RetrySettingsConfig;
hideThinkingBlock?: boolean;
showMessageTimestamps?: boolean; // default: false - prefix each rendered message with its local clock time
smoothStreaming?: boolean; // default: true
smoothStreamingFps?: number; // default: 60, clamped to 30-120 when read
showCacheMissNotices?: boolean; // default: false - show prompt-cache miss and compaction cost notices
Expand Down Expand Up @@ -1526,6 +1527,10 @@ export class SettingsManager {
return this.settings.hideThinkingBlock ?? false;
}

getShowMessageTimestamps(): boolean {
return this.settings.showMessageTimestamps ?? false;
}

getSmoothStreaming(): boolean {
return this.settings.smoothStreaming ?? true;
}
Expand Down Expand Up @@ -1560,6 +1565,12 @@ export class SettingsManager {
this.save();
}

setShowMessageTimestamps(show: boolean): void {
this.globalSettings.showMessageTimestamps = show;
this.markModified("showMessageTimestamps");
this.save();
}

setSmoothStreaming(enabled: boolean): void {
this.globalSettings.smoothStreaming = enabled;
this.markModified("smoothStreaming");
Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,13 +361,12 @@ export {
type HostDaemonPaths,
InteractiveMode,
type InteractiveModeOptions,
isTransportGoneError,
type JsonAgentSessionEvent,
type ModelInfo,
PINNED_HOST_CLIENT_CAPABILITIES,
type PrintModeOptions,
RpcClient,
RpcTransportGoneError,
isTransportGoneError,
type RpcClientEvent,
type RpcClientOptions,
type RpcCommand,
Expand All @@ -377,6 +376,7 @@ export {
type RpcExtensionUIResponse,
type RpcResponse,
type RpcSessionState,
RpcTransportGoneError,
runPrintMode,
runRpcMode,
} from "./modes/index.ts";
Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/src/modes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ export {
PINNED_HOST_CLIENT_CAPABILITIES,
} from "./rpc/host-ensure.ts";
export {
isTransportGoneError,
type ModelInfo,
RpcClient,
RpcTransportGoneError,
isTransportGoneError,
type RpcClientEvent,
type RpcClientOptions,
type RpcEventListener,
RpcTransportGoneError,
} from "./rpc/rpc-client.ts";
export { runRpcMode } from "./rpc/rpc-mode.ts";
export type {
Expand Down
24 changes: 22 additions & 2 deletions packages/coding-agent/src/modes/interactive/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# changes

## 2026-08-30 - Render optional assistant message arrival timestamps

### What changed

- `packages/coding-agent/src/modes/interactive/components/assistant-message.ts`: stamps the first rendered content line with a dim local `HH:MM:SS` prefix when enabled, falls back to unprefixed rendering when a narrow viewport cannot fit the prefix plus content padding, preserves the first `updateContent()` arrival time across streaming updates, rerenders, and width changes, and toggles the prefix without rebuilding memoized content children.
- `packages/coding-agent/src/modes/interactive/components/settings-selector.ts`: exposes the default-off message timestamp toggle in `/settings`.
- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`: wires persisted state into new, restored, and streaming assistant messages; coordinates tool-separated render segments so one logical message receives exactly one prefix with a shared arrival time; keeps that ownership through an initially empty smooth-stream frame; applies toggles immediately; and refreshes the setting before transcript reconstruction on reload.

### Why

- Long-running interactive sessions need an unobtrusive way to distinguish when assistant output first arrived without changing existing output unless the operator opts in.

### Why an extension could not handle it

- Assistant component construction, streaming updates, transcript reconstruction, and the built-in settings selector are private interactive-mode rendering boundaries without an extension hook that can add a stable prefix to every applicable component.

### Expected merge conflict zones

- `packages/coding-agent/src/modes/interactive/components/assistant-message.ts`: outer render-cache invalidation, narrow-width fallback, and message update lifecycle.
- `packages/coding-agent/src/modes/interactive/components/settings-selector.ts`: settings list construction and callbacks.
- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`: assistant component construction, tool-separated timestamp eligibility, runtime settings, `/settings` callbacks, and reload transcript reconstruction.

## 2026-08-30 - Harden shared-host reconnect fallback

### What changed
Expand Down Expand Up @@ -34,7 +56,6 @@

- LOW: the entry-list reconciliation block inside `performRefresh()`.


## 2026-08-30 - Rebind the shared-host proxy on session_replaced

### What changed
Expand All @@ -53,7 +74,6 @@

- LOW: the wire-event switch in the proxy's `client.onEvent` handler, the replacement methods, and the `refresh` definition.


## 2026-08-30 - Scope host UI dispatch to the shared-host lane

### What changed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { AssistantMessage } from "@earendil-works/pi-ai";
import { type Component, Container, Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
import {
type Component,
Container,
Markdown,
type MarkdownTheme,
Spacer,
Text,
visibleWidth,
} from "@earendil-works/pi-tui";
import type { MarkdownTransformer } from "../../../core/extensions/types.ts";
import { getMarkdownTheme, theme } from "../theme/theme.ts";
import { type AssistantRenderDescriptor, createAssistantRenderDescriptors } from "./assistant-render-descriptors.ts";
Expand Down Expand Up @@ -28,6 +36,12 @@ export class AssistantMessageComponent extends Container {
private hasToolCalls = false;
private expanded = false;
private isStreaming = false;
private showTimestamps: boolean;
private timestampEligible: boolean;
// Stamped once when the message arrives, never at render time: render() is
// cache-backed and re-runs on resize, so reading the clock there would make a
// past message silently change its own timestamp.
private arrivedAt?: Date;

constructor(
message?: AssistantMessage,
Expand All @@ -36,9 +50,14 @@ export class AssistantMessageComponent extends Container {
hiddenThinkingLabel = "Thinking...",
outputPad = 1,
markdownTransformers: readonly MarkdownTransformer[] = [],
showTimestamps = false,
timestamp?: { readonly eligible: boolean; readonly arrivedAt?: Date },
) {
super();

this.showTimestamps = showTimestamps;
this.timestampEligible = timestamp?.eligible ?? true;
this.arrivedAt = timestamp?.arrivedAt;
this.hideThinkingBlock = hideThinkingBlock;
this.markdownTheme = markdownTheme;
this.hiddenThinkingLabel = hiddenThinkingLabel;
Expand All @@ -53,7 +72,7 @@ export class AssistantMessageComponent extends Container {
}

override invalidate(): void {
this.renderCache = undefined;
this.invalidateRenderCache();
super.invalidate();
this.renderDescriptors = [];
this.refreshContent();
Expand Down Expand Up @@ -83,13 +102,54 @@ export class AssistantMessageComponent extends Container {
this.refreshContent();
}

setShowTimestamps(show: boolean): void {
if (this.showTimestamps === show) return;
this.showTimestamps = show;
this.invalidateRenderCache();
}

setTimestampEligible(eligible: boolean): void {
if (this.timestampEligible === eligible) return;
this.timestampEligible = eligible;
this.invalidateRenderCache();
}

hasVisibleContent(): boolean {
return this.renderDescriptors.some((descriptor) => descriptor.kind !== "spacer");
}

getArrivedAt(): Date | undefined {
return this.arrivedAt;
}

private timestampPrefix(): string {
if (!this.showTimestamps || !this.timestampEligible) return "";
const at = this.arrivedAt ?? new Date();
const hh = String(at.getHours()).padStart(2, "0");
const mm = String(at.getMinutes()).padStart(2, "0");
const ss = String(at.getSeconds()).padStart(2, "0");
return theme.fg("dim", `${hh}:${mm}:${ss} `);
}

override render(width: number): string[] {
const signature = this.lastMessageSignature ?? "";
if (this.renderCache?.width === width && this.renderCache.signature === signature) {
return [...this.renderCache.lines];
}

const lines = super.render(width);
const prefix = this.timestampPrefix();
const prefixWidth = visibleWidth(prefix);
const showTimestamp = prefixWidth > 0 && width > prefixWidth;
let lines = super.render(showTimestamp ? width - prefixWidth : width);
const firstContentLineIndex = lines.findIndex((line) => visibleWidth(line) > 0);
if (firstContentLineIndex >= 0 && showTimestamp) {
const prefixedLine = prefix + lines[firstContentLineIndex];
if (visibleWidth(prefixedLine) <= width) {
lines[firstContentLineIndex] = prefixedLine;
} else {
lines = super.render(width);
}
}
if (this.hasToolCalls || lines.length === 0) {
this.cacheRender(width, signature, lines);
return lines;
Expand All @@ -102,6 +162,10 @@ export class AssistantMessageComponent extends Container {
}

updateContent(message: AssistantMessage, isStreaming = this.isStreaming): void {
// First delta only. updateContent runs once per streaming chunk, so stamping
// unconditionally would march the clock forward while the message is still
// being written and land on the completion time rather than the start.
this.arrivedAt ??= new Date();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve arrival times when reconstructing messages

When a saved session is resumed, reloaded, or otherwise rebuilt with timestamps enabled, constructing each component immediately calls updateContent(message), so this line stamps every historical message with the current reconstruction time and ignores the persisted message.timestamp. As a result, old messages display the resume/reload time—and can change again after settings-triggered transcript rebuilds—instead of a stable historical time; reconstructed components need to derive or restore a persisted arrival value while empty streaming components can continue stamping their first update.

Useful? React with 👍 / 👎.

@codeg-dev codeg-dev Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional for the contract in #1205: arrivedAt is the time a component first receives content in the current TUI, not the provider/model timestamp persisted on the message. Reusing message.timestamp would change that meaning, and the feature deliberately avoids adding persisted transcript schema. A restored or reloaded transcript is reconstructed into the current UI and receives its current UI-arrival time; live updates remain fixed by arrivedAt ??=. The regression suite locks the two-update stability contract.

const previousMessage = this.lastMessage;
const streamingChanged = this.isStreaming !== isStreaming;
this.isStreaming = isStreaming;
Expand Down Expand Up @@ -192,6 +256,10 @@ export class AssistantMessageComponent extends Container {
this.renderCache = { lines: [...lines], signature, width };
}

private invalidateRenderCache(): void {
this.renderCache = undefined;
}

private refreshContent(): void {
if (!this.lastMessage) return;
this.lastMessageSignature = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface SettingsConfig {
terminalTheme: TerminalTheme;
availableThemes: string[];
hideThinkingBlock: boolean;
showMessageTimestamps: boolean;
smoothStreaming: boolean;
smoothStreamingFps: number;
mermaidRenderingMode: MermaidRenderingMode;
Expand Down Expand Up @@ -110,6 +111,7 @@ export interface SettingsCallbacks {
onThemeChange: (theme: string) => void;
onThemePreview?: (theme: string) => void;
onHideThinkingBlockChange: (hidden: boolean) => void;
onShowMessageTimestampsChange: (show: boolean) => void;
onSmoothStreamingChange: (enabled: boolean) => void;
onSmoothStreamingFpsChange: (fps: number) => void;
onMermaidRenderingModeChange: (mode: MermaidRenderingMode) => void;
Expand Down Expand Up @@ -543,6 +545,13 @@ export class SettingsSelectorComponent extends Container {
currentValue: config.hideThinkingBlock ? "true" : "false",
values: ["true", "false"],
},
{
id: "message-timestamps",
label: "Message timestamps",
description: "Prefix each assistant message with the local time it arrived",
currentValue: config.showMessageTimestamps ? "true" : "false",
values: ["true", "false"],
},
{
id: "smooth-streaming",
label: "Smooth streaming",
Expand Down Expand Up @@ -835,6 +844,9 @@ export class SettingsSelectorComponent extends Container {
case "hide-thinking":
callbacks.onHideThinkingBlockChange(newValue === "true");
break;
case "message-timestamps":
callbacks.onShowMessageTimestampsChange(newValue === "true");
break;
case "smooth-streaming":
callbacks.onSmoothStreamingChange(newValue === "true");
break;
Expand Down
Loading