magi-ai 0.1.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.
- package/LICENSE +21 -0
- package/README.ja.md +377 -0
- package/README.md +377 -0
- package/dist/bin/magi-benchmark.d.ts +14 -0
- package/dist/bin/magi-benchmark.js +93 -0
- package/dist/bin/magi-mcp.d.ts +8 -0
- package/dist/bin/magi-mcp.js +28 -0
- package/dist/bin/magi.d.ts +2 -0
- package/dist/bin/magi.js +634 -0
- package/dist/src/adapters/base.d.ts +34 -0
- package/dist/src/adapters/base.js +149 -0
- package/dist/src/adapters/claude.d.ts +29 -0
- package/dist/src/adapters/claude.js +65 -0
- package/dist/src/adapters/codex.d.ts +21 -0
- package/dist/src/adapters/codex.js +41 -0
- package/dist/src/adapters/gemini.d.ts +18 -0
- package/dist/src/adapters/gemini.js +31 -0
- package/dist/src/adapters/registry.d.ts +19 -0
- package/dist/src/adapters/registry.js +59 -0
- package/dist/src/audit/hash-chain.d.ts +21 -0
- package/dist/src/audit/hash-chain.js +70 -0
- package/dist/src/audit/types.d.ts +25 -0
- package/dist/src/audit/types.js +1 -0
- package/dist/src/audit/writer.d.ts +18 -0
- package/dist/src/audit/writer.js +100 -0
- package/dist/src/benchmark/golden-tasks.d.ts +9 -0
- package/dist/src/benchmark/golden-tasks.js +476 -0
- package/dist/src/benchmark/reporter.d.ts +5 -0
- package/dist/src/benchmark/reporter.js +107 -0
- package/dist/src/benchmark/runner.d.ts +30 -0
- package/dist/src/benchmark/runner.js +224 -0
- package/dist/src/benchmark/scorer.d.ts +12 -0
- package/dist/src/benchmark/scorer.js +124 -0
- package/dist/src/benchmark/types.d.ts +54 -0
- package/dist/src/benchmark/types.js +1 -0
- package/dist/src/cache/deliberation-cache.d.ts +49 -0
- package/dist/src/cache/deliberation-cache.js +127 -0
- package/dist/src/cli/commands/config-cmd.d.ts +11 -0
- package/dist/src/cli/commands/config-cmd.js +190 -0
- package/dist/src/cli/commands/demo.d.ts +12 -0
- package/dist/src/cli/commands/demo.js +66 -0
- package/dist/src/cli/commands/setup.d.ts +7 -0
- package/dist/src/cli/commands/setup.js +182 -0
- package/dist/src/cli/i18n.d.ts +89 -0
- package/dist/src/cli/i18n.js +176 -0
- package/dist/src/cli/interactive-select.d.ts +27 -0
- package/dist/src/cli/interactive-select.js +130 -0
- package/dist/src/cli/tui-setup.d.ts +24 -0
- package/dist/src/cli/tui-setup.js +42 -0
- package/dist/src/config/cli-detector.d.ts +37 -0
- package/dist/src/config/cli-detector.js +99 -0
- package/dist/src/config/user-config.d.ts +81 -0
- package/dist/src/config/user-config.js +134 -0
- package/dist/src/context/auto-collector.d.ts +43 -0
- package/dist/src/context/auto-collector.js +337 -0
- package/dist/src/context/manager.d.ts +35 -0
- package/dist/src/context/manager.js +162 -0
- package/dist/src/context/serializer.d.ts +20 -0
- package/dist/src/context/serializer.js +52 -0
- package/dist/src/demo/recorded-deliberation.d.ts +13 -0
- package/dist/src/demo/recorded-deliberation.js +277 -0
- package/dist/src/engine/angel-detector.d.ts +83 -0
- package/dist/src/engine/angel-detector.js +334 -0
- package/dist/src/engine/at-field.d.ts +40 -0
- package/dist/src/engine/at-field.js +195 -0
- package/dist/src/engine/berserk-orchestrator.d.ts +66 -0
- package/dist/src/engine/berserk-orchestrator.js +378 -0
- package/dist/src/engine/change-metrics.d.ts +56 -0
- package/dist/src/engine/change-metrics.js +214 -0
- package/dist/src/engine/consensus.d.ts +20 -0
- package/dist/src/engine/consensus.js +146 -0
- package/dist/src/engine/dead-sea-scrolls.d.ts +132 -0
- package/dist/src/engine/dead-sea-scrolls.js +610 -0
- package/dist/src/engine/drift-detector.d.ts +39 -0
- package/dist/src/engine/drift-detector.js +225 -0
- package/dist/src/engine/dummy-plug.d.ts +44 -0
- package/dist/src/engine/dummy-plug.js +190 -0
- package/dist/src/engine/engram-manager.d.ts +55 -0
- package/dist/src/engine/engram-manager.js +306 -0
- package/dist/src/engine/events.d.ts +130 -0
- package/dist/src/engine/events.js +44 -0
- package/dist/src/engine/gospel.d.ts +30 -0
- package/dist/src/engine/gospel.js +129 -0
- package/dist/src/engine/hallucination-detector.d.ts +33 -0
- package/dist/src/engine/hallucination-detector.js +215 -0
- package/dist/src/engine/human-resolver.d.ts +19 -0
- package/dist/src/engine/human-resolver.js +89 -0
- package/dist/src/engine/instrumentality.d.ts +64 -0
- package/dist/src/engine/instrumentality.js +297 -0
- package/dist/src/engine/iruel-battle.d.ts +79 -0
- package/dist/src/engine/iruel-battle.js +319 -0
- package/dist/src/engine/kernel/deliberation-kernel.d.ts +12 -0
- package/dist/src/engine/kernel/deliberation-kernel.js +303 -0
- package/dist/src/engine/kernel/index.d.ts +8 -0
- package/dist/src/engine/kernel/index.js +7 -0
- package/dist/src/engine/kernel/phase-runner.d.ts +10 -0
- package/dist/src/engine/kernel/phase-runner.js +155 -0
- package/dist/src/engine/kernel/post-processor.d.ts +17 -0
- package/dist/src/engine/kernel/post-processor.js +131 -0
- package/dist/src/engine/kernel/types.d.ts +107 -0
- package/dist/src/engine/kernel/types.js +1 -0
- package/dist/src/engine/kernel/unit-executor.d.ts +6 -0
- package/dist/src/engine/kernel/unit-executor.js +132 -0
- package/dist/src/engine/lcl-manager.d.ts +44 -0
- package/dist/src/engine/lcl-manager.js +143 -0
- package/dist/src/engine/middleware/cache.d.ts +7 -0
- package/dist/src/engine/middleware/cache.js +29 -0
- package/dist/src/engine/middleware/chain.d.ts +18 -0
- package/dist/src/engine/middleware/chain.js +45 -0
- package/dist/src/engine/middleware/firewall.d.ts +8 -0
- package/dist/src/engine/middleware/firewall.js +24 -0
- package/dist/src/engine/middleware/index.d.ts +4 -0
- package/dist/src/engine/middleware/index.js +3 -0
- package/dist/src/engine/middleware/types.d.ts +43 -0
- package/dist/src/engine/middleware/types.js +1 -0
- package/dist/src/engine/nebuchadnezzar-key.d.ts +61 -0
- package/dist/src/engine/nebuchadnezzar-key.js +203 -0
- package/dist/src/engine/neon-genesis.d.ts +52 -0
- package/dist/src/engine/neon-genesis.js +203 -0
- package/dist/src/engine/objective-judge.d.ts +53 -0
- package/dist/src/engine/objective-judge.js +214 -0
- package/dist/src/engine/offline-mode.d.ts +18 -0
- package/dist/src/engine/offline-mode.js +46 -0
- package/dist/src/engine/orchestrator.d.ts +79 -0
- package/dist/src/engine/orchestrator.js +58 -0
- package/dist/src/engine/secret-cipher.d.ts +26 -0
- package/dist/src/engine/secret-cipher.js +114 -0
- package/dist/src/engine/seele-council.d.ts +90 -0
- package/dist/src/engine/seele-council.js +482 -0
- package/dist/src/engine/self-destruct.d.ts +61 -0
- package/dist/src/engine/self-destruct.js +231 -0
- package/dist/src/engine/self-evolution.d.ts +64 -0
- package/dist/src/engine/self-evolution.js +368 -0
- package/dist/src/engine/sync-rate.d.ts +45 -0
- package/dist/src/engine/sync-rate.js +151 -0
- package/dist/src/engine/type666-firewall.d.ts +76 -0
- package/dist/src/engine/type666-firewall.js +343 -0
- package/dist/src/engine/umbilical-cable.d.ts +41 -0
- package/dist/src/engine/umbilical-cable.js +192 -0
- package/dist/src/index.d.ts +106 -0
- package/dist/src/index.js +426 -0
- package/dist/src/mcp/server.d.ts +38 -0
- package/dist/src/mcp/server.js +196 -0
- package/dist/src/metrics/token-tracker.d.ts +38 -0
- package/dist/src/metrics/token-tracker.js +112 -0
- package/dist/src/parsers/json-extractor.d.ts +9 -0
- package/dist/src/parsers/json-extractor.js +239 -0
- package/dist/src/parsers/opinion-schema.d.ts +81 -0
- package/dist/src/parsers/opinion-schema.js +147 -0
- package/dist/src/parsers/unstructured-parser.d.ts +20 -0
- package/dist/src/parsers/unstructured-parser.js +122 -0
- package/dist/src/pipelines/architecture.d.ts +10 -0
- package/dist/src/pipelines/architecture.js +9 -0
- package/dist/src/pipelines/bug-analysis.d.ts +9 -0
- package/dist/src/pipelines/bug-analysis.js +8 -0
- package/dist/src/pipelines/code-review.d.ts +10 -0
- package/dist/src/pipelines/code-review.js +30 -0
- package/dist/src/pipelines/custom.d.ts +14 -0
- package/dist/src/pipelines/custom.js +29 -0
- package/dist/src/pipelines/registry.d.ts +9 -0
- package/dist/src/pipelines/registry.js +20 -0
- package/dist/src/prompts/personas.d.ts +6 -0
- package/dist/src/prompts/personas.js +44 -0
- package/dist/src/prompts/schemas.d.ts +4 -0
- package/dist/src/prompts/schemas.js +24 -0
- package/dist/src/prompts/templates.d.ts +6 -0
- package/dist/src/prompts/templates.js +91 -0
- package/dist/src/repl/accessibility.d.ts +23 -0
- package/dist/src/repl/accessibility.js +46 -0
- package/dist/src/repl/banner.d.ts +4 -0
- package/dist/src/repl/banner.js +28 -0
- package/dist/src/repl/boot-animation.d.ts +13 -0
- package/dist/src/repl/boot-animation.js +143 -0
- package/dist/src/repl/completer.d.ts +21 -0
- package/dist/src/repl/completer.js +168 -0
- package/dist/src/repl/context.d.ts +24 -0
- package/dist/src/repl/context.js +42 -0
- package/dist/src/repl/display-utils.d.ts +13 -0
- package/dist/src/repl/display-utils.js +65 -0
- package/dist/src/repl/event-listener.d.ts +18 -0
- package/dist/src/repl/event-listener.js +112 -0
- package/dist/src/repl/export-formatter.d.ts +8 -0
- package/dist/src/repl/export-formatter.js +73 -0
- package/dist/src/repl/ghost-text.d.ts +31 -0
- package/dist/src/repl/ghost-text.js +119 -0
- package/dist/src/repl/handoff-animation.d.ts +15 -0
- package/dist/src/repl/handoff-animation.js +65 -0
- package/dist/src/repl/history.d.ts +16 -0
- package/dist/src/repl/history.js +130 -0
- package/dist/src/repl/job-registry.d.ts +26 -0
- package/dist/src/repl/job-registry.js +80 -0
- package/dist/src/repl/magi-repl.d.ts +72 -0
- package/dist/src/repl/magi-repl.js +1008 -0
- package/dist/src/repl/multiline-input.d.ts +45 -0
- package/dist/src/repl/multiline-input.js +78 -0
- package/dist/src/repl/prompt-builder.d.ts +19 -0
- package/dist/src/repl/prompt-builder.js +36 -0
- package/dist/src/repl/repl-state.d.ts +5 -0
- package/dist/src/repl/repl-state.js +19 -0
- package/dist/src/repl/result-display.d.ts +8 -0
- package/dist/src/repl/result-display.js +195 -0
- package/dist/src/repl/session-stats.d.ts +26 -0
- package/dist/src/repl/session-stats.js +119 -0
- package/dist/src/repl/slash-commands.d.ts +60 -0
- package/dist/src/repl/slash-commands.js +725 -0
- package/dist/src/repl/terminal-sanitize.d.ts +14 -0
- package/dist/src/repl/terminal-sanitize.js +19 -0
- package/dist/src/reporters/console.d.ts +7 -0
- package/dist/src/reporters/console.js +78 -0
- package/dist/src/reporters/json.d.ts +2 -0
- package/dist/src/reporters/json.js +3 -0
- package/dist/src/reporters/markdown.d.ts +2 -0
- package/dist/src/reporters/markdown.js +65 -0
- package/dist/src/reporters/streaming.d.ts +20 -0
- package/dist/src/reporters/streaming.js +178 -0
- package/dist/src/tui/activity-log.d.ts +23 -0
- package/dist/src/tui/activity-log.js +67 -0
- package/dist/src/tui/animations.d.ts +39 -0
- package/dist/src/tui/animations.js +167 -0
- package/dist/src/tui/ansi.d.ts +28 -0
- package/dist/src/tui/ansi.js +51 -0
- package/dist/src/tui/boot-sequence.d.ts +11 -0
- package/dist/src/tui/boot-sequence.js +98 -0
- package/dist/src/tui/colors.d.ts +101 -0
- package/dist/src/tui/colors.js +71 -0
- package/dist/src/tui/header.d.ts +24 -0
- package/dist/src/tui/header.js +122 -0
- package/dist/src/tui/index.d.ts +3 -0
- package/dist/src/tui/index.js +3 -0
- package/dist/src/tui/keypress.d.ts +25 -0
- package/dist/src/tui/keypress.js +95 -0
- package/dist/src/tui/layout.d.ts +74 -0
- package/dist/src/tui/layout.js +171 -0
- package/dist/src/tui/magi-tui.d.ts +101 -0
- package/dist/src/tui/magi-tui.js +754 -0
- package/dist/src/tui/panel.d.ts +45 -0
- package/dist/src/tui/panel.js +292 -0
- package/dist/src/tui/screen-buffer.d.ts +54 -0
- package/dist/src/tui/screen-buffer.js +262 -0
- package/dist/src/tui/status-bar.d.ts +25 -0
- package/dist/src/tui/status-bar.js +124 -0
- package/dist/src/tui/terminal-detect.d.ts +26 -0
- package/dist/src/tui/terminal-detect.js +44 -0
- package/dist/src/tui/tui-helpers.d.ts +12 -0
- package/dist/src/tui/tui-helpers.js +37 -0
- package/dist/src/types/adapter.d.ts +75 -0
- package/dist/src/types/adapter.js +36 -0
- package/dist/src/types/config.d.ts +108 -0
- package/dist/src/types/config.js +85 -0
- package/dist/src/types/consensus.d.ts +55 -0
- package/dist/src/types/consensus.js +17 -0
- package/dist/src/types/core.d.ts +178 -0
- package/dist/src/types/core.js +85 -0
- package/dist/src/types/magi-api.d.ts +62 -0
- package/dist/src/types/magi-api.js +7 -0
- package/dist/src/types/phase-h.d.ts +142 -0
- package/dist/src/types/phase-h.js +7 -0
- package/dist/src/types/phase-i.d.ts +186 -0
- package/dist/src/types/phase-i.js +6 -0
- package/dist/src/types/phase-k.d.ts +259 -0
- package/dist/src/types/phase-k.js +6 -0
- package/dist/src/types/phase-l.d.ts +199 -0
- package/dist/src/types/phase-l.js +6 -0
- package/dist/src/types/pipeline.d.ts +37 -0
- package/dist/src/types/pipeline.js +2 -0
- package/dist/src/utils/abstain-factory.d.ts +2 -0
- package/dist/src/utils/abstain-factory.js +18 -0
- package/dist/src/utils/errors.d.ts +34 -0
- package/dist/src/utils/errors.js +59 -0
- package/dist/src/utils/file-validator.d.ts +50 -0
- package/dist/src/utils/file-validator.js +124 -0
- package/dist/src/utils/fire-and-forget.d.ts +5 -0
- package/dist/src/utils/fire-and-forget.js +10 -0
- package/dist/src/utils/flag-validator.d.ts +21 -0
- package/dist/src/utils/flag-validator.js +79 -0
- package/dist/src/utils/freeze.d.ts +8 -0
- package/dist/src/utils/freeze.js +16 -0
- package/dist/src/utils/language-detector.d.ts +16 -0
- package/dist/src/utils/language-detector.js +159 -0
- package/dist/src/utils/latency-tracker.d.ts +45 -0
- package/dist/src/utils/latency-tracker.js +100 -0
- package/dist/src/utils/logger.d.ts +33 -0
- package/dist/src/utils/logger.js +112 -0
- package/dist/src/utils/process.d.ts +40 -0
- package/dist/src/utils/process.js +253 -0
- package/dist/src/utils/retry.d.ts +12 -0
- package/dist/src/utils/retry.js +30 -0
- package/dist/src/utils/safe-fs.d.ts +38 -0
- package/dist/src/utils/safe-fs.js +56 -0
- package/dist/src/utils/safe-json-parse.d.ts +15 -0
- package/dist/src/utils/safe-json-parse.js +49 -0
- package/dist/src/utils/sanitize.d.ts +14 -0
- package/dist/src/utils/sanitize.js +186 -0
- package/dist/src/utils/semaphore.d.ts +22 -0
- package/dist/src/utils/semaphore.js +57 -0
- package/dist/src/utils/shutdown.d.ts +6 -0
- package/dist/src/utils/shutdown.js +51 -0
- package/dist/src/utils/tty.d.ts +5 -0
- package/dist/src/utils/tty.js +7 -0
- package/package.json +82 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAGI unit panel renderer.
|
|
3
|
+
* Draws bordered panels with unit name, status, and content.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScreenBuffer } from './screen-buffer.js';
|
|
6
|
+
import type { Rect, Connector } from './layout.js';
|
|
7
|
+
import { type RGBColor } from './colors.js';
|
|
8
|
+
export type PanelState = 'idle' | 'thinking' | 'complete' | 'error' | 'berserk' | 'offline';
|
|
9
|
+
export interface PanelContent {
|
|
10
|
+
unit: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
state: PanelState;
|
|
13
|
+
vote?: string;
|
|
14
|
+
confidence?: number;
|
|
15
|
+
durationMs?: number;
|
|
16
|
+
activeDurationMs?: number;
|
|
17
|
+
reasoning?: string;
|
|
18
|
+
breathPhase?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface MagiNodeContent {
|
|
21
|
+
phaseCode?: string;
|
|
22
|
+
roundLabel?: string;
|
|
23
|
+
modeLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function getDisplayName(unit: string): string;
|
|
26
|
+
export declare class PanelRenderer {
|
|
27
|
+
/** Draw a bordered panel with content. */
|
|
28
|
+
drawPanel(buf: ScreenBuffer, rect: Rect, content: PanelContent): void;
|
|
29
|
+
/** Draw panel border with box-drawing characters. */
|
|
30
|
+
drawBorder(buf: ScreenBuffer, rect: Rect, color: RGBColor, breathPhase?: number): void;
|
|
31
|
+
/** Draw panel inner content based on state. */
|
|
32
|
+
drawContent(buf: ScreenBuffer, rect: Rect, content: PanelContent): void;
|
|
33
|
+
/** Draw Y-shape connector lines. */
|
|
34
|
+
drawConnectors(buf: ScreenBuffer, connectors: Connector[], pulseIndex?: number): void;
|
|
35
|
+
/** Draw the central MAGI junction node. */
|
|
36
|
+
drawMagiNode(buf: ScreenBuffer, rect: Rect, content?: MagiNodeContent): void;
|
|
37
|
+
/** Fill the panel background with the unit's color when voting is complete. */
|
|
38
|
+
fillPanelBg(buf: ScreenBuffer, rect: Rect, bgColor: RGBColor): void;
|
|
39
|
+
private panelColor;
|
|
40
|
+
private basePanelFill;
|
|
41
|
+
drawUnitDetailOverlay(buf: ScreenBuffer, rect: Rect, content: PanelContent): void;
|
|
42
|
+
private writeCentered;
|
|
43
|
+
}
|
|
44
|
+
/** Strip common markdown formatting for TUI plain-text display. */
|
|
45
|
+
export declare function stripMarkdown(text: string): string;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAGI unit panel renderer.
|
|
3
|
+
* Draws bordered panels with unit name, status, and content.
|
|
4
|
+
*/
|
|
5
|
+
import { BOX, BOX_DOUBLE } from './layout.js';
|
|
6
|
+
import { EVA_PALETTE, unitColor, voteColor, adjustBrightness, mixColors } from './colors.js';
|
|
7
|
+
import { ATTR_NONE, ATTR_BOLD, ATTR_DIM, stringDisplayWidth, charDisplayWidth, truncateToDisplayWidth } from './screen-buffer.js';
|
|
8
|
+
import { formatElapsedMmSs } from './tui-helpers.js';
|
|
9
|
+
// ── Unit display names ──────────────────────────────────────
|
|
10
|
+
const UNIT_DISPLAY = {
|
|
11
|
+
MELCHIOR: 'MELCHIOR ・ 1',
|
|
12
|
+
BALTHASAR: 'BALTHASAR ・ 2',
|
|
13
|
+
CASPER: 'CASPER ・ 3',
|
|
14
|
+
};
|
|
15
|
+
export function getDisplayName(unit) {
|
|
16
|
+
return UNIT_DISPLAY[unit] ?? unit;
|
|
17
|
+
}
|
|
18
|
+
// ── Panel Renderer ──────────────────────────────────────────
|
|
19
|
+
export class PanelRenderer {
|
|
20
|
+
/** Draw a bordered panel with content. */
|
|
21
|
+
drawPanel(buf, rect, content) {
|
|
22
|
+
const color = this.panelColor(content);
|
|
23
|
+
this.fillPanelBg(buf, rect, this.basePanelFill(content));
|
|
24
|
+
this.drawBorder(buf, rect, color, content.state === 'thinking' ? content.breathPhase : undefined);
|
|
25
|
+
if (content.state === 'complete' && content.vote) {
|
|
26
|
+
this.fillPanelBg(buf, rect, mixColors(EVA_PALETTE.background, voteColor(content.vote), 0.16));
|
|
27
|
+
}
|
|
28
|
+
this.drawContent(buf, rect, content);
|
|
29
|
+
}
|
|
30
|
+
/** Draw panel border with box-drawing characters. */
|
|
31
|
+
drawBorder(buf, rect, color, breathPhase) {
|
|
32
|
+
const c = breathPhase !== undefined
|
|
33
|
+
? adjustBrightness(color, 0.85 + 0.15 * Math.sin(breathPhase * Math.PI * 2))
|
|
34
|
+
: color;
|
|
35
|
+
const { row, col, width, height } = rect;
|
|
36
|
+
// Top border
|
|
37
|
+
buf.writeAt(row, col, BOX.tl, c.r, c.g, c.b);
|
|
38
|
+
buf.writeAt(row, col + width - 1, BOX.tr, c.r, c.g, c.b);
|
|
39
|
+
for (let x = col + 1; x < col + width - 1; x++) {
|
|
40
|
+
buf.writeAt(row, x, BOX.h, c.r, c.g, c.b);
|
|
41
|
+
}
|
|
42
|
+
// Bottom border
|
|
43
|
+
buf.writeAt(row + height - 1, col, BOX.bl, c.r, c.g, c.b);
|
|
44
|
+
buf.writeAt(row + height - 1, col + width - 1, BOX.br, c.r, c.g, c.b);
|
|
45
|
+
for (let x = col + 1; x < col + width - 1; x++) {
|
|
46
|
+
buf.writeAt(row + height - 1, x, BOX.h, c.r, c.g, c.b);
|
|
47
|
+
}
|
|
48
|
+
// Side borders
|
|
49
|
+
for (let y = row + 1; y < row + height - 1; y++) {
|
|
50
|
+
buf.writeAt(y, col, BOX.v, c.r, c.g, c.b);
|
|
51
|
+
buf.writeAt(y, col + width - 1, BOX.v, c.r, c.g, c.b);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Draw panel inner content based on state. */
|
|
55
|
+
drawContent(buf, rect, content) {
|
|
56
|
+
const innerCol = rect.col + 2;
|
|
57
|
+
const innerWidth = rect.width - 4;
|
|
58
|
+
const innerRow = rect.row + 1;
|
|
59
|
+
const tc = EVA_PALETTE.textPrimary;
|
|
60
|
+
const tw = EVA_PALETTE.textSecondary;
|
|
61
|
+
// Title line (unit name)
|
|
62
|
+
const title = ` ${content.displayName} `;
|
|
63
|
+
const titleBg = mixColors(EVA_PALETTE.background, colorFromState(content), 0.24);
|
|
64
|
+
buf.writeAt(rect.row, rect.col + 2, title, tc.r, tc.g, tc.b, titleBg.r, titleBg.g, titleBg.b, ATTR_BOLD);
|
|
65
|
+
// Content area (clear first)
|
|
66
|
+
for (let y = innerRow; y < rect.row + rect.height - 1; y++) {
|
|
67
|
+
for (let x = rect.col + 1; x < rect.col + rect.width - 1; x++) {
|
|
68
|
+
buf.writeAt(y, x, ' ');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
switch (content.state) {
|
|
72
|
+
case 'idle':
|
|
73
|
+
this.writeCentered(buf, innerRow + 1, rect, '待 機', tc, ATTR_DIM);
|
|
74
|
+
this.writeCentered(buf, innerRow + 2, rect, 'STANDBY', tw, ATTR_DIM);
|
|
75
|
+
break;
|
|
76
|
+
case 'thinking': {
|
|
77
|
+
this.writeCentered(buf, innerRow + 1, rect, '審 議 中', tc, ATTR_BOLD);
|
|
78
|
+
this.writeCentered(buf, innerRow + 2, rect, 'DELIBERATING', tw, ATTR_DIM);
|
|
79
|
+
if (content.activeDurationMs !== undefined) {
|
|
80
|
+
this.writeCentered(buf, innerRow + 3, rect, `ACTIVE ${formatElapsedMmSs(content.activeDurationMs)}`, tw, ATTR_DIM);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case 'complete': {
|
|
85
|
+
const vc = voteColor(content.vote ?? 'ABSTAIN');
|
|
86
|
+
const voteLabel = content.vote === 'APPROVE' ? '承 認' : content.vote === 'REJECT' ? '否 定' : '保 留';
|
|
87
|
+
const confStr = content.confidence !== undefined ? `${Math.round(content.confidence * 100)}%` : '--';
|
|
88
|
+
const durStr = content.durationMs !== undefined ? `${(content.durationMs / 1000).toFixed(1)}s` : '--';
|
|
89
|
+
this.writeCentered(buf, innerRow, rect, bracketedVote(content.vote ?? 'ABSTAIN'), vc, ATTR_BOLD);
|
|
90
|
+
this.writeCentered(buf, innerRow + 1, rect, voteLabel, vc, ATTR_BOLD);
|
|
91
|
+
this.writeCentered(buf, innerRow + 2, rect, `SYNC ${confStr} TIME ${durStr}`, tw, ATTR_DIM);
|
|
92
|
+
// Reasoning excerpt — start immediately after vote line
|
|
93
|
+
if (content.reasoning) {
|
|
94
|
+
const maxReasonLines = Math.min(3, Math.max(0, rect.height - 6));
|
|
95
|
+
if (maxReasonLines > 0) {
|
|
96
|
+
const stripped = stripMarkdown(content.reasoning);
|
|
97
|
+
const lines = wrapText(stripped, innerWidth);
|
|
98
|
+
const hasOverflow = lines.length > maxReasonLines;
|
|
99
|
+
const visibleCount = Math.min(lines.length, maxReasonLines);
|
|
100
|
+
for (let i = 0; i < visibleCount; i++) {
|
|
101
|
+
let line = lines[i];
|
|
102
|
+
if (hasOverflow && i === visibleCount - 1) {
|
|
103
|
+
// Force overflow indicator; avoid double-ellipsis if truncate already added one
|
|
104
|
+
const truncated = truncateToDisplayWidth(line, innerWidth - 1);
|
|
105
|
+
line = truncated.endsWith('\u2026') ? truncated : truncated + '\u2026';
|
|
106
|
+
}
|
|
107
|
+
buf.writeAt(innerRow + 4 + i, innerCol, line, tw.r, tw.g, tw.b);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case 'error': {
|
|
114
|
+
const wc = EVA_PALETTE.warning;
|
|
115
|
+
this.writeCentered(buf, innerRow + 1, rect, '侵 食', wc, ATTR_BOLD);
|
|
116
|
+
this.writeCentered(buf, innerRow + 2, rect, 'INTRUSION', wc, ATTR_DIM);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'berserk': {
|
|
120
|
+
const wc = EVA_PALETTE.warning;
|
|
121
|
+
this.writeCentered(buf, innerRow + 1, rect, '暴 走', wc, ATTR_BOLD);
|
|
122
|
+
this.writeCentered(buf, innerRow + 2, rect, 'B E R S E R K', wc, ATTR_BOLD);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'offline': {
|
|
126
|
+
const gc = EVA_PALETTE.abstain;
|
|
127
|
+
this.writeCentered(buf, innerRow + 1, rect, '切 断', gc, ATTR_DIM);
|
|
128
|
+
this.writeCentered(buf, innerRow + 2, rect, 'DISCONNECTED', gc, ATTR_DIM);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Draw Y-shape connector lines. */
|
|
134
|
+
drawConnectors(buf, connectors, pulseIndex = -1) {
|
|
135
|
+
const c = EVA_PALETTE.frame;
|
|
136
|
+
const pulseColor = EVA_PALETTE.approve;
|
|
137
|
+
for (let i = 0; i < connectors.length; i++) {
|
|
138
|
+
const conn = connectors[i];
|
|
139
|
+
const color = i <= pulseIndex ? pulseColor : c;
|
|
140
|
+
buf.writeAt(conn.row, conn.col, conn.char, color.r, color.g, color.b);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Draw the central MAGI junction node. */
|
|
144
|
+
drawMagiNode(buf, rect, content) {
|
|
145
|
+
const accent = content?.modeLabel === 'BERSERK'
|
|
146
|
+
? EVA_PALETTE.warning
|
|
147
|
+
: EVA_PALETTE.magi;
|
|
148
|
+
const c = accent;
|
|
149
|
+
const { row, col, width } = rect;
|
|
150
|
+
// Border
|
|
151
|
+
buf.writeAt(row, col, BOX.tl, c.r, c.g, c.b);
|
|
152
|
+
buf.writeAt(row, col + width - 1, BOX.tr, c.r, c.g, c.b);
|
|
153
|
+
for (let x = col + 1; x < col + width - 1; x++) {
|
|
154
|
+
buf.writeAt(row, x, BOX.h, c.r, c.g, c.b);
|
|
155
|
+
}
|
|
156
|
+
buf.writeAt(row + 2, col, BOX.bl, c.r, c.g, c.b);
|
|
157
|
+
buf.writeAt(row + 2, col + width - 1, BOX.br, c.r, c.g, c.b);
|
|
158
|
+
for (let x = col + 1; x < col + width - 1; x++) {
|
|
159
|
+
buf.writeAt(row + 2, x, BOX.h, c.r, c.g, c.b);
|
|
160
|
+
}
|
|
161
|
+
buf.writeAt(row + 1, col, BOX.v, c.r, c.g, c.b);
|
|
162
|
+
buf.writeAt(row + 1, col + width - 1, BOX.v, c.r, c.g, c.b);
|
|
163
|
+
// "MAGI" text
|
|
164
|
+
const nodeLabel = content?.phaseCode
|
|
165
|
+
? truncateToDisplayWidth(`${content.phaseCode} ${content.roundLabel ?? ''}`.trim(), width - 2)
|
|
166
|
+
: 'MAGI';
|
|
167
|
+
const innerCol = col + Math.max(1, Math.floor((width - stringDisplayWidth(nodeLabel)) / 2));
|
|
168
|
+
buf.writeAt(row + 1, innerCol, nodeLabel, c.r, c.g, c.b, 0, 0, 0, ATTR_BOLD);
|
|
169
|
+
}
|
|
170
|
+
/** Fill the panel background with the unit's color when voting is complete. */
|
|
171
|
+
fillPanelBg(buf, rect, bgColor) {
|
|
172
|
+
for (let y = rect.row + 1; y < rect.row + rect.height - 1; y++) {
|
|
173
|
+
for (let x = rect.col + 1; x < rect.col + rect.width - 1; x++) {
|
|
174
|
+
const cell = ' ';
|
|
175
|
+
buf.writeAt(y, x, cell, 255, 255, 255, bgColor.r, bgColor.g, bgColor.b);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
panelColor(content) {
|
|
180
|
+
switch (content.state) {
|
|
181
|
+
case 'error': return EVA_PALETTE.warning;
|
|
182
|
+
case 'berserk': return EVA_PALETTE.warning;
|
|
183
|
+
case 'offline': return EVA_PALETTE.abstain;
|
|
184
|
+
default: return unitColor(content.unit);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
basePanelFill(content) {
|
|
188
|
+
const accent = colorFromState(content);
|
|
189
|
+
return mixColors(EVA_PALETTE.background, accent, content.state === 'thinking' ? 0.12 : 0.08);
|
|
190
|
+
}
|
|
191
|
+
drawUnitDetailOverlay(buf, rect, content) {
|
|
192
|
+
const c = unitColor(content.unit);
|
|
193
|
+
const title = `${content.displayName} DETAIL`;
|
|
194
|
+
const text = content.reasoning ? stripMarkdown(content.reasoning) : 'No detail available.';
|
|
195
|
+
for (let y = rect.row; y < rect.row + rect.height; y++) {
|
|
196
|
+
for (let x = rect.col; x < rect.col + rect.width; x++) {
|
|
197
|
+
buf.writeAt(y, x, ' ', EVA_PALETTE.textSecondary.r, EVA_PALETTE.textSecondary.g, EVA_PALETTE.textSecondary.b, 0, 0, 0);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
for (let x = rect.col; x < rect.col + rect.width; x++) {
|
|
201
|
+
buf.writeAt(rect.row, x, BOX_DOUBLE.h, c.r, c.g, c.b);
|
|
202
|
+
buf.writeAt(rect.row + rect.height - 1, x, BOX_DOUBLE.h, c.r, c.g, c.b);
|
|
203
|
+
}
|
|
204
|
+
for (let y = rect.row + 1; y < rect.row + rect.height - 1; y++) {
|
|
205
|
+
buf.writeAt(y, rect.col, BOX_DOUBLE.v, c.r, c.g, c.b);
|
|
206
|
+
buf.writeAt(y, rect.col + rect.width - 1, BOX_DOUBLE.v, c.r, c.g, c.b);
|
|
207
|
+
}
|
|
208
|
+
buf.writeAt(rect.row, rect.col, BOX_DOUBLE.tl, c.r, c.g, c.b);
|
|
209
|
+
buf.writeAt(rect.row, rect.col + rect.width - 1, BOX_DOUBLE.tr, c.r, c.g, c.b);
|
|
210
|
+
buf.writeAt(rect.row + rect.height - 1, rect.col, BOX_DOUBLE.bl, c.r, c.g, c.b);
|
|
211
|
+
buf.writeAt(rect.row + rect.height - 1, rect.col + rect.width - 1, BOX_DOUBLE.br, c.r, c.g, c.b);
|
|
212
|
+
buf.writeAt(rect.row, rect.col + 2, ` ${truncateToDisplayWidth(title, rect.width - 8)} `, c.r, c.g, c.b, 0, 0, 0, ATTR_BOLD);
|
|
213
|
+
const meta = truncateToDisplayWidth(`${content.vote ?? 'UNKNOWN'} CONF:${content.confidence !== undefined ? Math.round(content.confidence * 100) : '--'}% TIME:${content.durationMs !== undefined ? `${(content.durationMs / 1000).toFixed(1)}s` : '--'}`, rect.width - 4);
|
|
214
|
+
buf.writeAt(rect.row + 2, rect.col + 2, meta, EVA_PALETTE.frame.r, EVA_PALETTE.frame.g, EVA_PALETTE.frame.b);
|
|
215
|
+
const lines = wrapText(text, rect.width - 4);
|
|
216
|
+
const maxLines = rect.height - 6;
|
|
217
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
218
|
+
let line = lines[i];
|
|
219
|
+
if (lines.length > maxLines && i === maxLines - 1) {
|
|
220
|
+
const truncated = truncateToDisplayWidth(line, rect.width - 5);
|
|
221
|
+
line = truncated.endsWith('\u2026') ? truncated : truncated + '\u2026';
|
|
222
|
+
}
|
|
223
|
+
buf.writeAt(rect.row + 4 + i, rect.col + 2, line, EVA_PALETTE.textSecondary.r, EVA_PALETTE.textSecondary.g, EVA_PALETTE.textSecondary.b);
|
|
224
|
+
}
|
|
225
|
+
buf.writeAt(rect.row + rect.height - 2, rect.col + 2, 'ESC / q : close 1 / 2 / 3 : switch unit', c.r, c.g, c.b);
|
|
226
|
+
}
|
|
227
|
+
writeCentered(buf, row, rect, text, color, attrs = ATTR_NONE) {
|
|
228
|
+
const x = rect.col + Math.max(1, Math.floor((rect.width - stringDisplayWidth(text)) / 2));
|
|
229
|
+
buf.writeAt(row, x, text, color.r, color.g, color.b, 0, 0, 0, attrs);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function colorFromState(content) {
|
|
233
|
+
if (content.state === 'complete' && content.vote)
|
|
234
|
+
return voteColor(content.vote);
|
|
235
|
+
if (content.state === 'error' || content.state === 'berserk')
|
|
236
|
+
return EVA_PALETTE.warning;
|
|
237
|
+
if (content.state === 'offline')
|
|
238
|
+
return EVA_PALETTE.abstain;
|
|
239
|
+
return unitColor(content.unit);
|
|
240
|
+
}
|
|
241
|
+
function bracketedVote(vote) {
|
|
242
|
+
return vote === 'APPROVE'
|
|
243
|
+
? '<< APPROVE >>'
|
|
244
|
+
: vote === 'REJECT'
|
|
245
|
+
? '<< REJECT >>'
|
|
246
|
+
: '<< ABSTAIN >>';
|
|
247
|
+
}
|
|
248
|
+
// ── Markdown stripping ──────────────────────────────────────
|
|
249
|
+
/** Strip common markdown formatting for TUI plain-text display. */
|
|
250
|
+
export function stripMarkdown(text) {
|
|
251
|
+
return text
|
|
252
|
+
.replace(/^```[^\n]*\n?/gm, '') // fenced code block delimiters only (keep content)
|
|
253
|
+
.replace(/^#{1,6}\s+/gm, '') // heading markers (line-anchored)
|
|
254
|
+
.replace(/\*\*(.+?)\*\*/g, '$1') // bold **text**
|
|
255
|
+
.replace(/__(.+?)__/g, '$1') // bold __text__
|
|
256
|
+
.replace(/(?<=^|\s)\*([^\s*].*?[^\s*])\*(?=\s|$)/gm, '$1') // italic *text*
|
|
257
|
+
.replace(/`([^`]+)`/g, '$1') // inline code `text`
|
|
258
|
+
.replace(/^[-*+]\s/gm, '・ ') // bullet lists → ・ (with space)
|
|
259
|
+
.replace(/\n{3,}/g, '\n\n') // collapse excessive blank lines
|
|
260
|
+
.trim();
|
|
261
|
+
}
|
|
262
|
+
// ── Text wrapping ────────────────────────────────────────────
|
|
263
|
+
/** Wrap a text string into lines that fit within maxWidth display columns (CJK-aware). */
|
|
264
|
+
function wrapText(text, maxWidth) {
|
|
265
|
+
if (maxWidth <= 0)
|
|
266
|
+
return [];
|
|
267
|
+
const result = [];
|
|
268
|
+
// First split on explicit newlines, then wrap each segment
|
|
269
|
+
for (const segment of text.split('\n')) {
|
|
270
|
+
if (segment.length === 0) {
|
|
271
|
+
result.push('');
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
let line = '';
|
|
275
|
+
let lineWidth = 0;
|
|
276
|
+
for (const ch of segment) {
|
|
277
|
+
const cw = charDisplayWidth(ch.codePointAt(0) ?? 0);
|
|
278
|
+
if (lineWidth + cw > maxWidth) {
|
|
279
|
+
result.push(line);
|
|
280
|
+
line = ch;
|
|
281
|
+
lineWidth = cw;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
line += ch;
|
|
285
|
+
lineWidth += cw;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (line)
|
|
289
|
+
result.push(line);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Double-buffered virtual screen with diff-based rendering.
|
|
3
|
+
* Only changed cells are written to the terminal.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ATTR_NONE = 0;
|
|
6
|
+
export declare const ATTR_BOLD = 1;
|
|
7
|
+
export declare const ATTR_DIM = 2;
|
|
8
|
+
export interface Cell {
|
|
9
|
+
char: string;
|
|
10
|
+
fgR: number;
|
|
11
|
+
fgG: number;
|
|
12
|
+
fgB: number;
|
|
13
|
+
bgR: number;
|
|
14
|
+
bgG: number;
|
|
15
|
+
bgB: number;
|
|
16
|
+
attrs: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function charDisplayWidth(code: number): 0 | 1 | 2;
|
|
19
|
+
export declare function stringDisplayWidth(str: string): number;
|
|
20
|
+
/** Truncate text to fit within maxWidth display columns (CJK-aware). */
|
|
21
|
+
export declare function truncateToDisplayWidth(text: string, maxWidth: number): string;
|
|
22
|
+
export declare class ScreenBuffer {
|
|
23
|
+
private front;
|
|
24
|
+
private back;
|
|
25
|
+
cols: number;
|
|
26
|
+
rows: number;
|
|
27
|
+
constructor(cols: number, rows: number);
|
|
28
|
+
private createGrid;
|
|
29
|
+
/**
|
|
30
|
+
* Write text at (row, col) with optional colors and attributes.
|
|
31
|
+
* Row/col are 0-based. Out-of-bounds writes are clipped.
|
|
32
|
+
*/
|
|
33
|
+
writeAt(row: number, col: number, text: string, fgR?: number, fgG?: number, fgB?: number, bgR?: number, bgG?: number, bgB?: number, attrs?: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* Write a raw ANSI-colored string at (row, col).
|
|
36
|
+
* ANSI codes are stripped for cell placement; only the visible text occupies cells.
|
|
37
|
+
* Note: this is a convenience for simple colored text. For full control, use writeAt.
|
|
38
|
+
*/
|
|
39
|
+
writeRaw(row: number, col: number, text: string): void;
|
|
40
|
+
/** Clear a rectangular region in the back buffer. */
|
|
41
|
+
clearRect(row: number, col: number, width: number, height: number): void;
|
|
42
|
+
/** Clear the entire back buffer. */
|
|
43
|
+
clear(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Compute diff between front and back, return ANSI string.
|
|
46
|
+
* Does NOT call stdout.write — caller decides when to flush.
|
|
47
|
+
* After flush, back becomes the new front.
|
|
48
|
+
*/
|
|
49
|
+
flush(): string;
|
|
50
|
+
/** Resize both buffers (contents are lost). */
|
|
51
|
+
resize(cols: number, rows: number): void;
|
|
52
|
+
/** Force full redraw on next flush by clearing front buffer. */
|
|
53
|
+
invalidate(): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Double-buffered virtual screen with diff-based rendering.
|
|
3
|
+
* Only changed cells are written to the terminal.
|
|
4
|
+
*/
|
|
5
|
+
import { moveTo, fg, bg, RESET, BOLD, DIM, SYNC_BEGIN, SYNC_END } from './ansi.js';
|
|
6
|
+
// ── Attribute bitmask ───────────────────────────────────────
|
|
7
|
+
export const ATTR_NONE = 0;
|
|
8
|
+
export const ATTR_BOLD = 1;
|
|
9
|
+
export const ATTR_DIM = 2;
|
|
10
|
+
function emptyCell() {
|
|
11
|
+
return { char: ' ', fgR: 255, fgG: 255, fgB: 255, bgR: 0, bgG: 0, bgB: 0, attrs: ATTR_NONE };
|
|
12
|
+
}
|
|
13
|
+
function cellsEqual(a, b) {
|
|
14
|
+
return a.char === b.char
|
|
15
|
+
&& a.fgR === b.fgR && a.fgG === b.fgG && a.fgB === b.fgB
|
|
16
|
+
&& a.bgR === b.bgR && a.bgG === b.bgG && a.bgB === b.bgB
|
|
17
|
+
&& a.attrs === b.attrs;
|
|
18
|
+
}
|
|
19
|
+
// ── CJK width detection (simplified) ────────────────────────
|
|
20
|
+
export function charDisplayWidth(code) {
|
|
21
|
+
// Control characters
|
|
22
|
+
if (code <= 0x1F || (code >= 0x7F && code <= 0x9F))
|
|
23
|
+
return 0;
|
|
24
|
+
// Combining diacritical marks
|
|
25
|
+
if ((code >= 0x0300 && code <= 0x036F) ||
|
|
26
|
+
(code >= 0x1AB0 && code <= 0x1AFF) ||
|
|
27
|
+
(code >= 0x1DC0 && code <= 0x1DFF) ||
|
|
28
|
+
(code >= 0x20D0 && code <= 0x20FF) ||
|
|
29
|
+
(code >= 0xFE20 && code <= 0xFE2F))
|
|
30
|
+
return 0;
|
|
31
|
+
// Zero-width characters (ZWSP, ZWNJ, ZWJ, BOM) and Variation Selectors
|
|
32
|
+
if (code === 0x200B || code === 0x200C || code === 0x200D || code === 0xFEFF)
|
|
33
|
+
return 0;
|
|
34
|
+
if (code >= 0xFE00 && code <= 0xFE0F)
|
|
35
|
+
return 0; // Variation Selectors
|
|
36
|
+
// CJK and fullwidth ranges (width 2)
|
|
37
|
+
if ((code >= 0x1100 && code <= 0x115F) || // Hangul Jamo
|
|
38
|
+
(code >= 0x2E80 && code <= 0x303E) || // CJK Radicals, Kangxi, Symbols
|
|
39
|
+
(code >= 0x3041 && code <= 0x33BF) || // Hiragana, Katakana, CJK compatibility
|
|
40
|
+
(code >= 0x3400 && code <= 0x4DBF) || // CJK Extension A
|
|
41
|
+
(code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified Ideographs
|
|
42
|
+
(code >= 0xA000 && code <= 0xA4CF) || // Yi
|
|
43
|
+
(code >= 0xAC00 && code <= 0xD7AF) || // Hangul Syllables
|
|
44
|
+
(code >= 0xF900 && code <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
45
|
+
(code >= 0xFE30 && code <= 0xFE4F) || // CJK Compatibility Forms
|
|
46
|
+
(code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
|
|
47
|
+
(code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth Signs
|
|
48
|
+
(code >= 0x20000 && code <= 0x2FA1F) // CJK Extension B-F
|
|
49
|
+
)
|
|
50
|
+
return 2;
|
|
51
|
+
// Emoji ranges (width 2) — supplementary plane emoji that reliably render double-width
|
|
52
|
+
if ((code >= 0x1F300 && code <= 0x1F9FF) || // Misc Symbols & Pictographs, Emoticons, Supp Symbols
|
|
53
|
+
(code >= 0x1FA00 && code <= 0x1FA6F) || // Chess Symbols, Extended-A
|
|
54
|
+
(code >= 0x1FA70 && code <= 0x1FAFF)) // Symbols and Pictographs Extended-A
|
|
55
|
+
return 2;
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
export function stringDisplayWidth(str) {
|
|
59
|
+
let width = 0;
|
|
60
|
+
for (const ch of str) {
|
|
61
|
+
width += charDisplayWidth(ch.codePointAt(0) ?? 0);
|
|
62
|
+
}
|
|
63
|
+
return width;
|
|
64
|
+
}
|
|
65
|
+
/** Truncate text to fit within maxWidth display columns (CJK-aware). */
|
|
66
|
+
export function truncateToDisplayWidth(text, maxWidth) {
|
|
67
|
+
let width = 0;
|
|
68
|
+
let result = '';
|
|
69
|
+
for (const ch of text) {
|
|
70
|
+
const cw = charDisplayWidth(ch.codePointAt(0) ?? 0);
|
|
71
|
+
if (width + cw > maxWidth) {
|
|
72
|
+
if (width + 1 <= maxWidth)
|
|
73
|
+
result += '…';
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
result += ch;
|
|
77
|
+
width += cw;
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
// ── ScreenBuffer ────────────────────────────────────────────
|
|
82
|
+
export class ScreenBuffer {
|
|
83
|
+
front;
|
|
84
|
+
back;
|
|
85
|
+
cols;
|
|
86
|
+
rows;
|
|
87
|
+
constructor(cols, rows) {
|
|
88
|
+
this.cols = cols;
|
|
89
|
+
this.rows = rows;
|
|
90
|
+
this.front = this.createGrid();
|
|
91
|
+
this.back = this.createGrid();
|
|
92
|
+
}
|
|
93
|
+
createGrid() {
|
|
94
|
+
return Array.from({ length: this.rows }, () => Array.from({ length: this.cols }, emptyCell));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Write text at (row, col) with optional colors and attributes.
|
|
98
|
+
* Row/col are 0-based. Out-of-bounds writes are clipped.
|
|
99
|
+
*/
|
|
100
|
+
writeAt(row, col, text, fgR = 255, fgG = 255, fgB = 255, bgR = 0, bgG = 0, bgB = 0, attrs = ATTR_NONE) {
|
|
101
|
+
if (row < 0 || row >= this.rows)
|
|
102
|
+
return;
|
|
103
|
+
let c = col;
|
|
104
|
+
for (const ch of text) {
|
|
105
|
+
if (c >= this.cols)
|
|
106
|
+
break;
|
|
107
|
+
if (c < 0) {
|
|
108
|
+
c += charDisplayWidth(ch.codePointAt(0) ?? 0);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const w = charDisplayWidth(ch.codePointAt(0) ?? 0);
|
|
112
|
+
// Zero-width characters (combining marks, ZWJ, etc.) — skip without advancing
|
|
113
|
+
if (w === 0)
|
|
114
|
+
continue;
|
|
115
|
+
const backRow = this.back[row];
|
|
116
|
+
const existing = backRow[c];
|
|
117
|
+
// HIGH-11: Wide char would overflow rightmost column — replace with space
|
|
118
|
+
if (w === 2 && c + 1 >= this.cols) {
|
|
119
|
+
backRow[c] = { char: ' ', fgR, fgG, fgB, bgR, bgG, bgB, attrs };
|
|
120
|
+
c += 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Clean up orphaned cells when overwriting wide characters.
|
|
124
|
+
// Placeholder cells (char='') are only created by wide char writes (w===2),
|
|
125
|
+
// so checking backRow[c+1].char==='' reliably identifies wide char continuation.
|
|
126
|
+
// Case 1: overwriting first cell of existing wide char with narrow char → clear placeholder
|
|
127
|
+
if (w === 1 && c + 1 < this.cols && backRow[c + 1].char === '' && existing.char !== '') {
|
|
128
|
+
backRow[c + 1] = emptyCell();
|
|
129
|
+
}
|
|
130
|
+
// Case 2: overwriting placeholder (second half of wide char) → clear first half
|
|
131
|
+
if (c > 0 && existing.char === '') {
|
|
132
|
+
backRow[c - 1] = emptyCell();
|
|
133
|
+
}
|
|
134
|
+
backRow[c] = { char: ch, fgR, fgG, fgB, bgR, bgG, bgB, attrs };
|
|
135
|
+
// For wide chars, fill the next cell with a placeholder
|
|
136
|
+
if (w === 2 && c + 1 < this.cols) {
|
|
137
|
+
backRow[c + 1] = { char: '', fgR, fgG, fgB, bgR, bgG, bgB, attrs };
|
|
138
|
+
}
|
|
139
|
+
c += w;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Write a raw ANSI-colored string at (row, col).
|
|
144
|
+
* ANSI codes are stripped for cell placement; only the visible text occupies cells.
|
|
145
|
+
* Note: this is a convenience for simple colored text. For full control, use writeAt.
|
|
146
|
+
*/
|
|
147
|
+
writeRaw(row, col, text) {
|
|
148
|
+
if (row < 0 || row >= this.rows)
|
|
149
|
+
return;
|
|
150
|
+
// Strip ANSI for display width calculation is not needed here —
|
|
151
|
+
// callers should use writeAt with explicit colors instead.
|
|
152
|
+
this.writeAt(row, col, text);
|
|
153
|
+
}
|
|
154
|
+
/** Clear a rectangular region in the back buffer. */
|
|
155
|
+
clearRect(row, col, width, height) {
|
|
156
|
+
for (let r = row; r < row + height && r < this.rows; r++) {
|
|
157
|
+
if (r < 0)
|
|
158
|
+
continue;
|
|
159
|
+
for (let c = col; c < col + width && c < this.cols; c++) {
|
|
160
|
+
if (c < 0)
|
|
161
|
+
continue;
|
|
162
|
+
this.back[r][c] = emptyCell();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Clear the entire back buffer. */
|
|
167
|
+
clear() {
|
|
168
|
+
this.clearRect(0, 0, this.cols, this.rows);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Compute diff between front and back, return ANSI string.
|
|
172
|
+
* Does NOT call stdout.write — caller decides when to flush.
|
|
173
|
+
* After flush, back becomes the new front.
|
|
174
|
+
*/
|
|
175
|
+
flush() {
|
|
176
|
+
let out = SYNC_BEGIN;
|
|
177
|
+
let lastRow = -1;
|
|
178
|
+
let lastCol = -1;
|
|
179
|
+
// HIGH-13: Track current styling state to avoid redundant escape sequences
|
|
180
|
+
let curFgR = -1, curFgG = -1, curFgB = -1;
|
|
181
|
+
let curBgR = -1, curBgG = -1, curBgB = -1;
|
|
182
|
+
let curAttrs = -1;
|
|
183
|
+
let styled = false; // whether any style has been emitted (needs final RESET)
|
|
184
|
+
for (let r = 0; r < this.rows; r++) {
|
|
185
|
+
const backRow = this.back[r];
|
|
186
|
+
const frontRow = this.front[r];
|
|
187
|
+
for (let c = 0; c < this.cols; c++) {
|
|
188
|
+
const backCell = backRow[c];
|
|
189
|
+
const frontCell = frontRow[c];
|
|
190
|
+
if (cellsEqual(backCell, frontCell))
|
|
191
|
+
continue;
|
|
192
|
+
// Skip wide-char placeholder cells — the terminal already advanced
|
|
193
|
+
// the cursor past this position when the wide char was printed.
|
|
194
|
+
// Emitting a space here would fragment CJK text (e.g. "補 完" instead of "補完").
|
|
195
|
+
if (backCell.char === '') {
|
|
196
|
+
frontRow[c] = { ...backCell };
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
// Position cursor if not contiguous
|
|
200
|
+
if (r !== lastRow || c !== lastCol) {
|
|
201
|
+
out += moveTo(r + 1, c + 1); // moveTo is 1-based
|
|
202
|
+
}
|
|
203
|
+
// Build cell output — only emit escape codes when they differ from current state
|
|
204
|
+
const needsAttrChange = backCell.attrs !== curAttrs;
|
|
205
|
+
if (needsAttrChange) {
|
|
206
|
+
// Attrs changed — must reset first to clear bold/dim, then re-apply
|
|
207
|
+
if (styled)
|
|
208
|
+
out += RESET;
|
|
209
|
+
curFgR = -1;
|
|
210
|
+
curFgG = -1;
|
|
211
|
+
curFgB = -1; // force re-emit after reset
|
|
212
|
+
curBgR = -1;
|
|
213
|
+
curBgG = -1;
|
|
214
|
+
curBgB = -1;
|
|
215
|
+
if (backCell.attrs & ATTR_BOLD)
|
|
216
|
+
out += BOLD;
|
|
217
|
+
if (backCell.attrs & ATTR_DIM)
|
|
218
|
+
out += DIM;
|
|
219
|
+
curAttrs = backCell.attrs;
|
|
220
|
+
}
|
|
221
|
+
if (backCell.fgR !== curFgR || backCell.fgG !== curFgG || backCell.fgB !== curFgB) {
|
|
222
|
+
out += fg(backCell.fgR, backCell.fgG, backCell.fgB);
|
|
223
|
+
curFgR = backCell.fgR;
|
|
224
|
+
curFgG = backCell.fgG;
|
|
225
|
+
curFgB = backCell.fgB;
|
|
226
|
+
}
|
|
227
|
+
if (backCell.bgR !== curBgR || backCell.bgG !== curBgG || backCell.bgB !== curBgB) {
|
|
228
|
+
out += bg(backCell.bgR, backCell.bgG, backCell.bgB);
|
|
229
|
+
curBgR = backCell.bgR;
|
|
230
|
+
curBgG = backCell.bgG;
|
|
231
|
+
curBgB = backCell.bgB;
|
|
232
|
+
}
|
|
233
|
+
out += backCell.char;
|
|
234
|
+
styled = true;
|
|
235
|
+
// Copy to front
|
|
236
|
+
frontRow[c] = { ...backCell };
|
|
237
|
+
lastRow = r;
|
|
238
|
+
lastCol = c + charDisplayWidth(backCell.char.codePointAt(0) ?? 0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Emit a final RESET to leave terminal in clean state
|
|
242
|
+
if (styled)
|
|
243
|
+
out += RESET;
|
|
244
|
+
out += SYNC_END;
|
|
245
|
+
return out;
|
|
246
|
+
}
|
|
247
|
+
/** Resize both buffers (contents are lost). */
|
|
248
|
+
resize(cols, rows) {
|
|
249
|
+
this.cols = cols;
|
|
250
|
+
this.rows = rows;
|
|
251
|
+
this.front = this.createGrid();
|
|
252
|
+
this.back = this.createGrid();
|
|
253
|
+
}
|
|
254
|
+
/** Force full redraw on next flush by clearing front buffer. */
|
|
255
|
+
invalidate() {
|
|
256
|
+
for (let r = 0; r < this.rows; r++) {
|
|
257
|
+
for (let c = 0; c < this.cols; c++) {
|
|
258
|
+
this.front[r][c] = { ...emptyCell(), char: '\x00' }; // sentinel
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bottom status bar renderer.
|
|
3
|
+
* Shows phase, round, elapsed time, and system indicators.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScreenBuffer } from './screen-buffer.js';
|
|
6
|
+
import type { Rect } from './layout.js';
|
|
7
|
+
import type { ConsensusDecision } from '../types/consensus.js';
|
|
8
|
+
export interface StatusData {
|
|
9
|
+
phase: string;
|
|
10
|
+
roundNumber: number;
|
|
11
|
+
maxRounds: number;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
decision?: ConsensusDecision;
|
|
14
|
+
fromCache?: boolean;
|
|
15
|
+
atFieldLevel?: number | 'MAX';
|
|
16
|
+
warning?: string;
|
|
17
|
+
showPrompt?: boolean;
|
|
18
|
+
modeLabel?: string;
|
|
19
|
+
alertCode?: string;
|
|
20
|
+
umbilicalStatus?: string;
|
|
21
|
+
syncSummary?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class StatusBarRenderer {
|
|
24
|
+
drawStatusBar(buf: ScreenBuffer, rect: Rect, data: StatusData): void;
|
|
25
|
+
}
|