refactor: codebase audit cleanup — dead code, dedup, module splits
Multi-agent audit + aggressive cleanup across server/web/coder/booterm, delivered behind a DEFER discipline so none of the in-flight files were touched. Removes dead code/deps/columns, dedups server + coder helpers, and splits the oversized modules (tools.ts, opencode-server.ts, sentinel-summaries, turn.ts, TerminalPane.tsx) behind stable contracts. Adds 78 parity/unit tests (server 587, coder 323); fixes two latent bugs (ChatPane queue keys, FileViewerOverlay blank-line parity). Intended tag: v2.7.12-audit-cleanup.
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
/** User messages are inserted atomically — never stream-append like assistant deltas. */
|
||||
export function applyMessageDelta(
|
||||
role: 'user' | 'assistant' | 'system' | 'tool',
|
||||
existingContent: string,
|
||||
chunk: string,
|
||||
): string {
|
||||
if (role === 'user') {
|
||||
return chunk || existingContent;
|
||||
}
|
||||
return existingContent + chunk;
|
||||
}
|
||||
@@ -7,12 +7,3 @@ export function isCoderSessionName(name: string | null | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Optimistic coder pane shell before scoped chat id arrives from the server. */
|
||||
export function defaultCoderWorkspacePane(id: string = crypto.randomUUID()) {
|
||||
return {
|
||||
id,
|
||||
kind: 'coder' as const,
|
||||
chatIds: [] as string[],
|
||||
activeChatIdx: -1,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,6 +63,3 @@ export function mergeWireToolCall(
|
||||
return [...list, entry];
|
||||
}
|
||||
|
||||
export function wireToolCallsToRuns(wires: CoderToolCallWire[] | undefined): ToolRun[] {
|
||||
return (wires ?? []).map(wireToolCallToRun);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
export function formatTokens(n: number | null | undefined): string | null {
|
||||
if (n === null || n === undefined) return null;
|
||||
if (n < 1000) return `${n} tok`;
|
||||
return `${(n / 1000).toFixed(1)}k tok`;
|
||||
// Short "Xs/m/h/d/mo/y" relative time without "ago" suffix (ProjectSidebar).
|
||||
export function relTime(iso: string): string {
|
||||
const now = Date.now();
|
||||
const t = Date.parse(iso);
|
||||
if (Number.isNaN(t)) return '';
|
||||
const sec = Math.max(0, Math.floor((now - t) / 1000));
|
||||
if (sec < 60) return `${sec}s`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min}m`;
|
||||
const hr = Math.floor(min / 60);
|
||||
if (hr < 24) return `${hr}h`;
|
||||
const day = Math.floor(hr / 24);
|
||||
if (day < 30) return `${day}d`;
|
||||
const mo = Math.floor(day / 30);
|
||||
if (mo < 12) return `${mo}mo`;
|
||||
return `${Math.floor(mo / 12)}y`;
|
||||
}
|
||||
|
||||
// "just now / Xm ago / Xh ago / Xd ago" with locale-date fallback for >7d
|
||||
// (SessionLandingPage).
|
||||
export function formatRelative(iso: string): string {
|
||||
const then = new Date(iso).getTime();
|
||||
if (Number.isNaN(then)) return '';
|
||||
const s = Math.max(0, Math.round((Date.now() - then) / 1000));
|
||||
if (s < 60) return 'just now';
|
||||
const m = Math.round(s / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
const h = Math.round(m / 60);
|
||||
if (h < 24) return `${h}h ago`;
|
||||
const d = Math.round(h / 24);
|
||||
if (d < 7) return `${d}d ago`;
|
||||
return new Date(iso).toLocaleDateString();
|
||||
}
|
||||
|
||||
// "just now / Xm ago / Xh ago / Xd ago" with no full-date fallback (AgentPicker).
|
||||
export function formatAgo(iso: string): string {
|
||||
const then = new Date(iso).getTime();
|
||||
if (Number.isNaN(then)) return '—';
|
||||
const diff = Date.now() - then;
|
||||
if (diff < 60_000) return 'just now';
|
||||
if (diff < 3_600_000) return `${Math.round(diff / 60_000)}m ago`;
|
||||
if (diff < 86_400_000) return `${Math.round(diff / 3_600_000)}h ago`;
|
||||
return `${Math.round(diff / 86_400_000)}d ago`;
|
||||
}
|
||||
|
||||
34
apps/web/src/lib/linkify-paths.tsx
Normal file
34
apps/web/src/lib/linkify-paths.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { sessionEvents } from '@/hooks/sessionEvents';
|
||||
|
||||
// Match path-shaped substrings ending in `.ext`. Requires a `/` in the match
|
||||
// to reduce false positives in prose (e.g. plain `foo.ts` won't match but
|
||||
// `src/foo.ts` will). False positives at the edges are accepted (2026-05-14).
|
||||
const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g;
|
||||
|
||||
export function linkifyPaths(text: string, keyPrefix = 'p'): ReactNode {
|
||||
const out: ReactNode[] = [];
|
||||
let lastIdx = 0;
|
||||
let idx = 0;
|
||||
for (const match of text.matchAll(PATH_REGEX)) {
|
||||
const matchedText = match[0];
|
||||
const start = match.index ?? 0;
|
||||
if (!matchedText.includes('/')) continue;
|
||||
if (start > lastIdx) out.push(text.slice(lastIdx, start));
|
||||
out.push(
|
||||
<button
|
||||
key={`${keyPrefix}-${idx}`}
|
||||
type="button"
|
||||
onClick={() => sessionEvents.emit({ type: 'open_file_in_browser', path: matchedText })}
|
||||
className="text-primary underline cursor-pointer hover:text-primary/80"
|
||||
>
|
||||
{matchedText}
|
||||
</button>
|
||||
);
|
||||
lastIdx = start + matchedText.length;
|
||||
idx += 1;
|
||||
}
|
||||
if (out.length === 0) return text;
|
||||
if (lastIdx < text.length) out.push(text.slice(lastIdx));
|
||||
return out;
|
||||
}
|
||||
46
apps/web/src/lib/terminal-protocol.ts
Normal file
46
apps/web/src/lib/terminal-protocol.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Terminal WebSocket wire protocol (centralized; v2 Phase 9 extraction).
|
||||
//
|
||||
// The booterm WS multiplexes two directions on one socket with a binary/text
|
||||
// discriminator (mirrored server-side in apps/booterm):
|
||||
// - PTY input (keystrokes, paste, hotkey bytes) is sent as a BINARY frame.
|
||||
// - Control frames are JSON text: outbound {type:'resize',cols,rows};
|
||||
// inbound {type:'init'} and {type:'exit',code}.
|
||||
// This module is the single source of that encoding so a server-side protocol
|
||||
// change is mirrored in one place. Behavior is byte-identical to the prior
|
||||
// inline encoding scattered across TerminalPane.
|
||||
|
||||
// TextEncoder is stateless; a single shared instance is equivalent to the
|
||||
// per-call `new TextEncoder()` the inline sites used.
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
// Keystrokes / paste / hotkey bytes go out as a BINARY frame so the server can
|
||||
// disambiguate them from JSON control frames. TextEncoder is in every modern
|
||||
// browser.
|
||||
export function encodeInput(text: string): Uint8Array {
|
||||
return textEncoder.encode(text);
|
||||
}
|
||||
|
||||
// Resize is in-band on the WebSocket as a JSON text frame. The HTTP /resize
|
||||
// endpoint had a race with PTY-map registration; WS frames don't.
|
||||
export function encodeResize(cols: number, rows: number): string {
|
||||
return JSON.stringify({ type: 'resize', cols, rows });
|
||||
}
|
||||
|
||||
export type ServerControlFrame =
|
||||
| { type: 'init' }
|
||||
| { type: 'exit'; code: number };
|
||||
|
||||
// Parse an inbound text frame. Returns a recognized control frame, or `null`
|
||||
// when the text is not JSON or not a known control type — in which case the
|
||||
// caller writes it to the terminal as raw text. Preserves the original
|
||||
// try/catch fall-through: a parse error or an unknown `type` both yield null.
|
||||
export function parseServerFrame(data: string): ServerControlFrame | null {
|
||||
try {
|
||||
const parsed = JSON.parse(data) as { type?: string; code?: number };
|
||||
if (parsed.type === 'init') return { type: 'init' };
|
||||
if (parsed.type === 'exit') return { type: 'exit', code: parsed.code ?? 0 };
|
||||
} catch {
|
||||
/* not JSON — caller writes as text */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user