@cdoing/opentuicli 0.1.21 → 0.1.26

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.
@@ -1,183 +0,0 @@
1
- /**
2
- * Sidebar — collapsible right panel with sections.
3
- * Uses single text per line to avoid layout concatenation issues.
4
- */
5
-
6
- import { TextAttributes } from "@opentui/core";
7
- import { useTheme } from "../context/theme";
8
-
9
- const W = 34; // content width (inside the border)
10
-
11
- export interface SidebarProps {
12
- provider: string;
13
- model: string;
14
- workingDir: string;
15
- tokens?: { input: number; output: number };
16
- contextPercent?: number;
17
- activeTool?: string;
18
- status: string;
19
- sessionTitle?: string;
20
- themeId?: string;
21
- modifiedFiles?: Array<{ path: string; additions: number; deletions: number }>;
22
- }
23
-
24
- function trunc(str: string, max: number): string {
25
- return str.length > max ? str.slice(0, max - 1) + "…" : str;
26
- }
27
-
28
- function bar(pct: number, len: number): string {
29
- const filled = Math.round((pct / 100) * len);
30
- return "█".repeat(filled) + "░".repeat(len - filled);
31
- }
32
-
33
- export function Sidebar(props: SidebarProps) {
34
- const { theme } = useTheme();
35
- const t = theme;
36
-
37
- const home = process.env.HOME || "";
38
- const dir = home && props.workingDir.startsWith(home)
39
- ? "~" + props.workingDir.slice(home.length) : props.workingDir;
40
-
41
- const pct = props.contextPercent ? Math.round(props.contextPercent) : 0;
42
- const pctColor = pct > 75 ? t.error : pct > 50 ? t.warning : t.success;
43
- const inTok = props.tokens ? props.tokens.input.toLocaleString() : "0";
44
- const outTok = props.tokens ? props.tokens.output.toLocaleString() : "0";
45
- const statusColor = props.status === "Error" ? t.error
46
- : props.status === "Processing..." ? t.warning : t.success;
47
-
48
- // Each line is a single string rendered in one <text> to avoid concatenation issues
49
- const lines: Array<{ text: string; fg: any; bold?: boolean }> = [];
50
-
51
- const sep = () => lines.push({ text: "│", fg: t.border });
52
- const header = (title: string) => {
53
- lines.push({ text: `│ ${title}`, fg: t.primary, bold: true });
54
- };
55
- const row = (label: string, value: string, fg?: any) => {
56
- const padded = label ? `│ ${label.padEnd(10)} ${trunc(value, W - 14)}` : `│ ${trunc(value, W - 4)}`;
57
- lines.push({ text: padded, fg: fg || t.text });
58
- };
59
- const shortcut = (key: string, label: string) => {
60
- lines.push({ text: `│ ${key.padEnd(10)} ${label}`, fg: t.textDim });
61
- };
62
-
63
- // ── Session ──
64
- header("Session");
65
- row("", props.sessionTitle || "New Session");
66
- row("Dir", trunc(dir, W - 14));
67
- row("Provider", props.provider);
68
- row("Model", props.model);
69
- if (props.themeId) row("Theme", props.themeId);
70
- sep();
71
-
72
- // ── Context ──
73
- header("Context");
74
- row("Input", `${inTok} tokens`);
75
- row("Output", `${outTok} tokens`);
76
- lines.push({ text: `│ ${bar(pct, 16)} ${pct}%`, fg: pctColor });
77
- sep();
78
-
79
- // ── Activity ──
80
- header("Activity");
81
- lines.push({ text: `│ Status ${props.status}`, fg: statusColor });
82
- if (props.activeTool) {
83
- lines.push({ text: `│ Tool ${trunc(props.activeTool, W - 14)}`, fg: t.toolRunning });
84
- }
85
- sep();
86
-
87
- // ── Modified Files ──
88
- if (props.modifiedFiles && props.modifiedFiles.length > 0) {
89
- header(`Files (${props.modifiedFiles.length})`);
90
- for (const f of props.modifiedFiles.slice(0, 6)) {
91
- const name = f.path.split("/").pop() || f.path;
92
- const diff = (f.additions > 0 ? ` +${f.additions}` : "") + (f.deletions > 0 ? ` -${f.deletions}` : "");
93
- lines.push({ text: `│ ${trunc(name, W - 12)}${diff}`, fg: t.text });
94
- }
95
- if (props.modifiedFiles.length > 6) {
96
- lines.push({ text: `│ … ${props.modifiedFiles.length - 6} more`, fg: t.textDim });
97
- }
98
- sep();
99
- }
100
-
101
- // ── LSP ──
102
- header("LSP");
103
- row("", "LSPs will activate as files are read");
104
- sep();
105
-
106
- // ── Shortcuts ──
107
- header("Shortcuts");
108
- shortcut("Ctrl+P", "Commands");
109
- shortcut("Ctrl+O", "Model");
110
- shortcut("Ctrl+T", "Theme");
111
- shortcut("Ctrl+N", "New session");
112
- shortcut("Ctrl+S", "Sessions");
113
- shortcut("Ctrl+B", "Sidebar");
114
- shortcut("F1", "Help");
115
-
116
- // ── Getting started card ──
117
- const cardW = W - 2;
118
- const cardLines: Array<{ text: string; fg: any; bold?: boolean }> = [];
119
- cardLines.push({ text: ` ◆ Getting started`, fg: t.primary, bold: true });
120
- cardLines.push({ text: ``, fg: t.textDim });
121
- cardLines.push({ text: ` Cdoing includes free models`, fg: t.textMuted });
122
- cardLines.push({ text: ` so you can start immediately.`, fg: t.textMuted });
123
- cardLines.push({ text: ``, fg: t.textDim });
124
- cardLines.push({ text: ` Connect from 75+ providers to`, fg: t.textMuted });
125
- cardLines.push({ text: ` use other models, including`, fg: t.textMuted });
126
- cardLines.push({ text: ` Claude, GPT, Gemini etc`, fg: t.textMuted });
127
- cardLines.push({ text: ``, fg: t.textDim });
128
- cardLines.push({ text: ` Connect provider /setup`, fg: t.text, bold: true });
129
-
130
- return (
131
- <box width={W + 2} flexDirection="column" backgroundColor={t.bgSubtle}>
132
- <box flexDirection="column" flexGrow={1}>
133
- {lines.map((line, i) => (
134
- <box key={i} height={1}>
135
- <text
136
- fg={line.fg}
137
- attributes={line.bold ? TextAttributes.BOLD : undefined}
138
- >
139
- {line.text}
140
- </text>
141
- </box>
142
- ))}
143
- {/* Fill remaining space */}
144
- <box flexGrow={1}>
145
- <text fg={t.border}>{"│"}</text>
146
- </box>
147
- {/* Getting started card at bottom */}
148
- <box flexDirection="column" paddingX={1}>
149
- <box height={1}>
150
- <text fg={t.border}>{"┌" + "─".repeat(cardW) + "┐"}</text>
151
- </box>
152
- {cardLines.map((cl, i) => (
153
- <box key={`card-${i}`} height={1}>
154
- <text fg={t.border}>{"│"}</text>
155
- <text fg={cl.fg} attributes={cl.bold ? TextAttributes.BOLD : undefined}>
156
- {cl.text.padEnd(cardW)}
157
- </text>
158
- <text fg={t.border}>{"│"}</text>
159
- </box>
160
- ))}
161
- <box height={1}>
162
- <text fg={t.border}>{"└" + "─".repeat(cardW) + "┘"}</text>
163
- </box>
164
- </box>
165
- {/* Working dir + version footer */}
166
- <box height={1} paddingX={1}>
167
- <text fg={t.textDim}>
168
- {trunc((() => {
169
- const home = process.env.HOME || "";
170
- return home && props.workingDir.startsWith(home)
171
- ? "~" + props.workingDir.slice(home.length) : props.workingDir;
172
- })(), W - 2)}
173
- </text>
174
- </box>
175
- <box height={1} paddingX={1}>
176
- <text fg={t.success}>{"● "}</text>
177
- <text fg={t.text} attributes={TextAttributes.BOLD}>{"Cdoing"}</text>
178
- <text fg={t.textDim}>{" Agent"}</text>
179
- </box>
180
- </box>
181
- </box>
182
- );
183
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * StatusBar — bottom bar with model info, tokens, context %, keybinds
3
- */
4
-
5
- import { TextAttributes } from "@opentui/core";
6
- import { useTheme } from "../context/theme";
7
-
8
- export interface StatusBarProps {
9
- provider: string;
10
- model: string;
11
- mode: string;
12
- workingDir: string;
13
- tokens?: { input: number; output: number };
14
- contextPercent?: number;
15
- activeTool?: string;
16
- isProcessing?: boolean;
17
- }
18
-
19
- export function StatusBar(props: StatusBarProps) {
20
- const { theme } = useTheme();
21
- const t = theme;
22
-
23
- const home = process.env.HOME || "";
24
- const shortDir = home && props.workingDir.startsWith(home)
25
- ? "~" + props.workingDir.slice(home.length)
26
- : props.workingDir;
27
-
28
- const tokenInfo = props.tokens
29
- ? ` ${props.tokens.input.toLocaleString()}→${props.tokens.output.toLocaleString()}`
30
- : "";
31
-
32
- const pct = props.contextPercent ? Math.round(props.contextPercent) : 0;
33
- const contextBar = pct > 0 ? ` ctx:${pct}%` : "";
34
-
35
- return (
36
- <box height={1} flexDirection="row" justifyContent="space-between" backgroundColor={t.bgSubtle}>
37
- <box flexDirection="row">
38
- <text fg={t.primary} attributes={TextAttributes.BOLD}>
39
- {` ${props.provider}`}
40
- </text>
41
- <text fg={t.textMuted}>{`/${props.model}`}</text>
42
- <text fg={t.textDim}>{" │ "}</text>
43
- <text fg={t.warning}>{props.mode}</text>
44
- {tokenInfo && (
45
- <>
46
- <text fg={t.textDim}>{" │"}</text>
47
- <text fg={t.textMuted}>{tokenInfo}</text>
48
- </>
49
- )}
50
- {contextBar && (
51
- <>
52
- <text fg={t.textDim}>{" │"}</text>
53
- <text fg={pct > 75 ? t.warning : t.textMuted}>{contextBar}</text>
54
- </>
55
- )}
56
- {props.activeTool && (
57
- <>
58
- <text fg={t.textDim}>{" │ "}</text>
59
- <text fg={t.toolRunning}>{`⏳ ${props.activeTool}`}</text>
60
- </>
61
- )}
62
- {props.isProcessing && !props.activeTool && (
63
- <>
64
- <text fg={t.textDim}>{" │ "}</text>
65
- <text fg={t.primary}>{"thinking..."}</text>
66
- </>
67
- )}
68
- </box>
69
- <box flexDirection="row">
70
- <text fg={t.textDim}>{shortDir}</text>
71
- <text fg={t.textDim}>{" │ "}</text>
72
- <text fg={t.textMuted}>{"^P:Commands ^O:Model ^C:Quit"}</text>
73
- </box>
74
- </box>
75
- );
76
- }
@@ -1,139 +0,0 @@
1
- /**
2
- * Toast Notification System — transient notifications for the TUI
3
- *
4
- * Provides a ToastProvider context and useToast hook for showing
5
- * auto-dismissing notifications with type-based styling.
6
- */
7
-
8
- import { createContext, useContext, useState, useEffect, useCallback, useRef } from "react";
9
- import type { ReactNode } from "react";
10
- import { useTheme } from "../context/theme";
11
- import type { Theme } from "../context/theme";
12
- import type { RGBA } from "@opentui/core";
13
-
14
- // ── Types ────────────────────────────────────────────
15
-
16
- export type ToastType = "info" | "success" | "warning" | "error";
17
-
18
- interface ToastItem {
19
- id: number;
20
- type: ToastType;
21
- message: string;
22
- duration: number;
23
- }
24
-
25
- interface ToastContextValue {
26
- toast: (type: ToastType, message: string, duration?: number) => void;
27
- }
28
-
29
- // ── Icons & Colors ───────────────────────────────────
30
-
31
- const TOAST_ICONS: Record<ToastType, string> = {
32
- info: "\u2139", // i
33
- success: "\u2713", // checkmark
34
- warning: "\u26A0", // warning sign
35
- error: "\u2717", // x mark
36
- };
37
-
38
- function getToastColor(type: ToastType, theme: Theme): RGBA {
39
- switch (type) {
40
- case "info": return theme.info;
41
- case "success": return theme.success;
42
- case "warning": return theme.warning;
43
- case "error": return theme.error;
44
- }
45
- }
46
-
47
- // ── Context ──────────────────────────────────────────
48
-
49
- const ToastContext = createContext<ToastContextValue | undefined>(undefined);
50
-
51
- const DEFAULT_DURATION = 3000;
52
- const MAX_VISIBLE = 3;
53
-
54
- // ── Provider ─────────────────────────────────────────
55
-
56
- export function ToastProvider(props: { children: ReactNode }) {
57
- const { theme } = useTheme();
58
- const [toasts, setToasts] = useState<ToastItem[]>([]);
59
- const idRef = useRef(0);
60
- const timersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
61
-
62
- const removeToast = useCallback((id: number) => {
63
- setToasts((prev) => prev.filter((t) => t.id !== id));
64
- const timer = timersRef.current.get(id);
65
- if (timer) {
66
- clearTimeout(timer);
67
- timersRef.current.delete(id);
68
- }
69
- }, []);
70
-
71
- const toast = useCallback((type: ToastType, message: string, duration?: number) => {
72
- const id = ++idRef.current;
73
- const dur = duration ?? DEFAULT_DURATION;
74
- const item: ToastItem = { id, type, message, duration: dur };
75
-
76
- setToasts((prev) => {
77
- const next = [...prev, item];
78
- // Keep only the most recent MAX_VISIBLE toasts
79
- if (next.length > MAX_VISIBLE) {
80
- const removed = next.splice(0, next.length - MAX_VISIBLE);
81
- // Clean up timers for removed toasts
82
- for (const r of removed) {
83
- const timer = timersRef.current.get(r.id);
84
- if (timer) {
85
- clearTimeout(timer);
86
- timersRef.current.delete(r.id);
87
- }
88
- }
89
- }
90
- return next;
91
- });
92
-
93
- // Set auto-dismiss timer
94
- const timer = setTimeout(() => {
95
- removeToast(id);
96
- }, dur);
97
- timersRef.current.set(id, timer);
98
- }, [removeToast]);
99
-
100
- // Clean up all timers on unmount
101
- useEffect(() => {
102
- return () => {
103
- for (const timer of timersRef.current.values()) {
104
- clearTimeout(timer);
105
- }
106
- timersRef.current.clear();
107
- };
108
- }, []);
109
-
110
- return (
111
- <ToastContext.Provider value={{ toast }}>
112
- {props.children}
113
- {/* Toast overlay — positioned at the bottom, above status bar */}
114
- {toasts.length > 0 && (
115
- <box flexDirection="column" alignItems="flex-end">
116
- {toasts.map((t) => {
117
- const color = getToastColor(t.type, theme);
118
- const icon = TOAST_ICONS[t.type];
119
- return (
120
- <box key={t.id} height={1} flexDirection="row" justifyContent="flex-end">
121
- <text fg={color}>{` ${icon} `}</text>
122
- <text fg={theme.text}>{t.message}</text>
123
- <text fg={theme.textDim}>{" "}</text>
124
- </box>
125
- );
126
- })}
127
- </box>
128
- )}
129
- </ToastContext.Provider>
130
- );
131
- }
132
-
133
- // ── Hook ─────────────────────────────────────────────
134
-
135
- export function useToast(): ToastContextValue {
136
- const ctx = useContext(ToastContext);
137
- if (!ctx) throw new Error("useToast must be used within ToastProvider");
138
- return ctx;
139
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * SDK Context — wraps AgentRunner, ToolRegistry, PermissionManager,
3
- * and exposes callbacks for runtime model/provider changes and agent rebuild.
4
- */
5
-
6
- import { createContext, useContext } from "react";
7
- import type { ReactNode } from "react";
8
- import type { AgentRunner } from "@cdoing/ai";
9
- import type { ToolRegistry, PermissionManager } from "@cdoing/core";
10
-
11
- export interface SDKState {
12
- agent: AgentRunner;
13
- registry: ToolRegistry;
14
- permissionManager: PermissionManager;
15
- workingDir: string;
16
- provider: string;
17
- model: string;
18
- /** Request a permission decision from the UI */
19
- requestPermission?: (toolName: string, message: string) => Promise<"allow" | "always" | "deny">;
20
- /** Rebuild the agent after model/provider change */
21
- rebuildAgent?: (provider: string, model: string, apiKey?: string) => void;
22
- /** Change working directory */
23
- setWorkingDir?: (dir: string) => void;
24
- }
25
-
26
- const SDKContext = createContext<SDKState | undefined>(undefined);
27
-
28
- export function SDKProvider(props: { value: SDKState; children: ReactNode }) {
29
- return (
30
- <SDKContext.Provider value={props.value}>
31
- {props.children}
32
- </SDKContext.Provider>
33
- );
34
- }
35
-
36
- export function useSDK() {
37
- const ctx = useContext(SDKContext);
38
- if (!ctx) throw new Error("useSDK must be used within SDKProvider");
39
- return ctx;
40
- }