@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.
- package/README.md +22 -13
- package/dist/cdoing-tui-darwin-arm64/bin/cdoing-tui +0 -0
- package/package.json +12 -9
- package/dist/index.js +0 -64
- package/dist/index.js.map +0 -7
- package/esbuild.config.cjs +0 -45
- package/src/app.tsx +0 -787
- package/src/components/dialog-command.tsx +0 -207
- package/src/components/dialog-help.tsx +0 -151
- package/src/components/dialog-model.tsx +0 -142
- package/src/components/dialog-status.tsx +0 -84
- package/src/components/dialog-theme.tsx +0 -318
- package/src/components/input-area.tsx +0 -380
- package/src/components/loading-spinner.tsx +0 -28
- package/src/components/message-list.tsx +0 -546
- package/src/components/permission-prompt.tsx +0 -72
- package/src/components/session-browser.tsx +0 -231
- package/src/components/session-footer.tsx +0 -30
- package/src/components/session-header.tsx +0 -39
- package/src/components/setup-wizard.tsx +0 -542
- package/src/components/sidebar.tsx +0 -183
- package/src/components/status-bar.tsx +0 -76
- package/src/components/toast.tsx +0 -139
- package/src/context/sdk.tsx +0 -40
- package/src/context/theme.tsx +0 -640
- package/src/index.ts +0 -50
- package/src/lib/autocomplete.ts +0 -262
- package/src/lib/context-providers.ts +0 -98
- package/src/lib/history.ts +0 -164
- package/src/lib/terminal-title.ts +0 -15
- package/src/routes/home.tsx +0 -148
- package/src/routes/session.tsx +0 -1309
- package/src/store/settings.ts +0 -107
- package/tsconfig.json +0 -23
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionBrowser — Interactive TUI overlay for browsing saved conversations
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Arrow keys to navigate
|
|
6
|
-
* - Enter to resume a conversation
|
|
7
|
-
* - d to delete, f to fork, v to view
|
|
8
|
-
* - Escape to close
|
|
9
|
-
* - Native <scrollbox> for smooth scrolling
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { TextAttributes } from "@opentui/core";
|
|
13
|
-
import { useState } from "react";
|
|
14
|
-
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
15
|
-
import { useTheme } from "../context/theme";
|
|
16
|
-
import {
|
|
17
|
-
listConversations,
|
|
18
|
-
deleteConversation,
|
|
19
|
-
forkConversation,
|
|
20
|
-
formatRelativeDate,
|
|
21
|
-
type Conversation,
|
|
22
|
-
} from "../lib/history";
|
|
23
|
-
|
|
24
|
-
export interface SessionBrowserProps {
|
|
25
|
-
onResume: (conv: Conversation) => void;
|
|
26
|
-
onClose: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function SessionBrowser(props: SessionBrowserProps) {
|
|
30
|
-
const { theme, customBg } = useTheme();
|
|
31
|
-
const t = theme;
|
|
32
|
-
const dims = useTerminalDimensions();
|
|
33
|
-
|
|
34
|
-
const [conversations, setConversations] = useState(() => listConversations());
|
|
35
|
-
const [selected, setSelected] = useState(0);
|
|
36
|
-
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
37
|
-
const [viewMode, setViewMode] = useState(false);
|
|
38
|
-
const [viewScroll, setViewScroll] = useState(0);
|
|
39
|
-
|
|
40
|
-
const maxVisible = Math.max(5, Math.floor((dims.height || 20) - 10));
|
|
41
|
-
|
|
42
|
-
useKeyboard((key: any) => {
|
|
43
|
-
if (viewMode) {
|
|
44
|
-
// View mode controls
|
|
45
|
-
if (key.name === "escape" || key.name === "q") {
|
|
46
|
-
setViewMode(false);
|
|
47
|
-
setViewScroll(0);
|
|
48
|
-
} else if (key.name === "up" || key.name === "k") {
|
|
49
|
-
setViewScroll((s) => Math.max(0, s - 1));
|
|
50
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
51
|
-
const conv = conversations[selected];
|
|
52
|
-
if (conv) {
|
|
53
|
-
const msgs = conv.messages.filter((m) => m.role !== "tool");
|
|
54
|
-
setViewScroll((s) => Math.min(msgs.length - 1, s + 1));
|
|
55
|
-
}
|
|
56
|
-
} else if (key.name === "return") {
|
|
57
|
-
const conv = conversations[selected];
|
|
58
|
-
if (conv) props.onResume(conv);
|
|
59
|
-
}
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (confirmDelete) {
|
|
64
|
-
if (key.name === "y") {
|
|
65
|
-
const conv = conversations[selected];
|
|
66
|
-
if (conv) {
|
|
67
|
-
deleteConversation(conv.id);
|
|
68
|
-
const updated = listConversations();
|
|
69
|
-
setConversations(updated);
|
|
70
|
-
setSelected((s) => Math.min(s, updated.length - 1));
|
|
71
|
-
}
|
|
72
|
-
setConfirmDelete(false);
|
|
73
|
-
} else {
|
|
74
|
-
setConfirmDelete(false);
|
|
75
|
-
}
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (key.name === "escape" || key.name === "q") {
|
|
80
|
-
props.onClose();
|
|
81
|
-
} else if (key.name === "up" || key.name === "k") {
|
|
82
|
-
setSelected((s) => Math.max(0, s - 1));
|
|
83
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
84
|
-
setSelected((s) => Math.min(conversations.length - 1, s + 1));
|
|
85
|
-
} else if (key.name === "return") {
|
|
86
|
-
const conv = conversations[selected];
|
|
87
|
-
if (conv) props.onResume(conv);
|
|
88
|
-
} else if (key.name === "d") {
|
|
89
|
-
if (conversations.length > 0) setConfirmDelete(true);
|
|
90
|
-
} else if (key.name === "f") {
|
|
91
|
-
const conv = conversations[selected];
|
|
92
|
-
if (conv) {
|
|
93
|
-
const forked = forkConversation(conv);
|
|
94
|
-
if (forked) {
|
|
95
|
-
setConversations(listConversations());
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} else if (key.name === "v") {
|
|
99
|
-
if (conversations.length > 0) {
|
|
100
|
-
setViewMode(true);
|
|
101
|
-
setViewScroll(0);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (conversations.length === 0) {
|
|
107
|
-
return (
|
|
108
|
-
<box
|
|
109
|
-
borderStyle="single"
|
|
110
|
-
borderColor={t.primary}
|
|
111
|
-
backgroundColor={customBg || t.bg}
|
|
112
|
-
paddingX={2}
|
|
113
|
-
paddingY={1}
|
|
114
|
-
flexDirection="column"
|
|
115
|
-
flexGrow={1}
|
|
116
|
-
>
|
|
117
|
-
<box flexDirection="row" flexShrink={0}>
|
|
118
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
119
|
-
{"Sessions"}
|
|
120
|
-
</text>
|
|
121
|
-
<text fg={t.textDim}>{"esc"}</text>
|
|
122
|
-
</box>
|
|
123
|
-
<text fg={t.textDim}>{"\nNo saved conversations.\n"}</text>
|
|
124
|
-
</box>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// View mode — show messages from selected conversation
|
|
129
|
-
if (viewMode) {
|
|
130
|
-
const conv = conversations[selected];
|
|
131
|
-
const msgs = conv ? conv.messages.filter((m) => m.role !== "tool") : [];
|
|
132
|
-
const visibleMsgs = msgs.slice(viewScroll, viewScroll + maxVisible);
|
|
133
|
-
const total = msgs.length;
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<box
|
|
137
|
-
borderStyle="single"
|
|
138
|
-
borderColor={t.primary}
|
|
139
|
-
paddingX={1}
|
|
140
|
-
paddingY={1}
|
|
141
|
-
flexDirection="column"
|
|
142
|
-
flexGrow={1}
|
|
143
|
-
>
|
|
144
|
-
<box flexDirection="row" flexShrink={0}>
|
|
145
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
146
|
-
{`Viewing: ${conv?.title || "Untitled"}`}
|
|
147
|
-
</text>
|
|
148
|
-
<text fg={t.textDim}>{"esc"}</text>
|
|
149
|
-
</box>
|
|
150
|
-
<text fg={t.textDim} flexShrink={0}>
|
|
151
|
-
{`${viewScroll + 1}–${Math.min(viewScroll + maxVisible, total)} of ${total} messages`}
|
|
152
|
-
</text>
|
|
153
|
-
<text flexShrink={0}>{""}</text>
|
|
154
|
-
<scrollbox flexGrow={1}>
|
|
155
|
-
<box flexShrink={0}>
|
|
156
|
-
{visibleMsgs.map((m, i) => {
|
|
157
|
-
const prefix = m.role === "user" ? "❯" : "◆";
|
|
158
|
-
const color = m.role === "user" ? t.success : t.text;
|
|
159
|
-
const content = m.content.length > 120 ? m.content.substring(0, 117) + "..." : m.content;
|
|
160
|
-
return (
|
|
161
|
-
<text key={`view-${viewScroll + i}`} fg={color}>
|
|
162
|
-
{` ${prefix} ${content.replace(/\n/g, " ")}`}
|
|
163
|
-
</text>
|
|
164
|
-
);
|
|
165
|
-
})}
|
|
166
|
-
</box>
|
|
167
|
-
</scrollbox>
|
|
168
|
-
<text flexShrink={0}>{""}</text>
|
|
169
|
-
<text fg={t.textMuted} flexShrink={0}>{" ↑↓ Scroll Enter Resume Esc Back"}</text>
|
|
170
|
-
</box>
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// List mode
|
|
175
|
-
return (
|
|
176
|
-
<box
|
|
177
|
-
borderStyle="single"
|
|
178
|
-
borderColor={t.primary}
|
|
179
|
-
backgroundColor={customBg || t.bg}
|
|
180
|
-
paddingX={1}
|
|
181
|
-
paddingY={1}
|
|
182
|
-
flexDirection="column"
|
|
183
|
-
flexGrow={1}
|
|
184
|
-
>
|
|
185
|
-
<box flexDirection="row" flexShrink={0}>
|
|
186
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD} flexGrow={1}>
|
|
187
|
-
{"Sessions"}
|
|
188
|
-
</text>
|
|
189
|
-
<text fg={t.textDim}>{"esc"}</text>
|
|
190
|
-
</box>
|
|
191
|
-
<text fg={t.textDim} flexShrink={0}>
|
|
192
|
-
{`${conversations.length} conversation${conversations.length !== 1 ? "s" : ""}`}
|
|
193
|
-
</text>
|
|
194
|
-
<text flexShrink={0}>{""}</text>
|
|
195
|
-
<scrollbox flexGrow={1}>
|
|
196
|
-
<box flexShrink={0}>
|
|
197
|
-
{conversations.map((conv, idx) => {
|
|
198
|
-
const isSelected = idx === selected;
|
|
199
|
-
const date = formatRelativeDate(conv.updatedAt);
|
|
200
|
-
const msgCount = conv.messages.filter((m) => m.role === "user").length;
|
|
201
|
-
const title = conv.title.length > 40 ? conv.title.substring(0, 37) + "..." : conv.title;
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
<box key={conv.id} flexDirection="row">
|
|
205
|
-
<text
|
|
206
|
-
fg={isSelected ? t.primary : t.textMuted}
|
|
207
|
-
attributes={isSelected ? TextAttributes.BOLD : undefined}
|
|
208
|
-
>
|
|
209
|
-
{` ${isSelected ? "❯" : " "} ${title}`}
|
|
210
|
-
</text>
|
|
211
|
-
<text fg={t.textDim}>
|
|
212
|
-
{` ${date} (${msgCount} msgs)`}
|
|
213
|
-
</text>
|
|
214
|
-
</box>
|
|
215
|
-
);
|
|
216
|
-
})}
|
|
217
|
-
</box>
|
|
218
|
-
</scrollbox>
|
|
219
|
-
{confirmDelete && (
|
|
220
|
-
<box flexShrink={0}>
|
|
221
|
-
<text>{""}</text>
|
|
222
|
-
<text fg={t.warning} attributes={TextAttributes.BOLD}>
|
|
223
|
-
{` Delete "${conversations[selected]?.title}"? (y/n)`}
|
|
224
|
-
</text>
|
|
225
|
-
</box>
|
|
226
|
-
)}
|
|
227
|
-
<text flexShrink={0}>{""}</text>
|
|
228
|
-
<text fg={t.textMuted} flexShrink={0}>{" ↑↓ Navigate Enter Resume v View d Delete f Fork Esc Close"}</text>
|
|
229
|
-
</box>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionFooter — bottom status line with directory and shortcut hints.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { useTheme } from "../context/theme";
|
|
6
|
-
|
|
7
|
-
export interface SessionFooterProps {
|
|
8
|
-
workingDir: string;
|
|
9
|
-
isProcessing: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function SessionFooter(props: SessionFooterProps) {
|
|
13
|
-
const { theme } = useTheme();
|
|
14
|
-
const t = theme;
|
|
15
|
-
|
|
16
|
-
const home = process.env.HOME || "";
|
|
17
|
-
const shortDir = home && props.workingDir.startsWith(home)
|
|
18
|
-
? "~" + props.workingDir.slice(home.length)
|
|
19
|
-
: props.workingDir;
|
|
20
|
-
|
|
21
|
-
const shortcuts = "^P:Commands ^O:Model ^T:Theme ^S:Sessions ^B:Sidebar";
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<box height={1} flexDirection="row" backgroundColor={t.bgSubtle}>
|
|
25
|
-
<text fg={t.textDim}>{` ${shortDir}`}</text>
|
|
26
|
-
<box flexGrow={1} />
|
|
27
|
-
<text fg={t.textMuted}>{`${shortcuts} `}</text>
|
|
28
|
-
</box>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionHeader — top-line session info bar.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { TextAttributes } from "@opentui/core";
|
|
6
|
-
import { useTheme } from "../context/theme";
|
|
7
|
-
|
|
8
|
-
export interface SessionHeaderProps {
|
|
9
|
-
title: string;
|
|
10
|
-
provider: string;
|
|
11
|
-
model: string;
|
|
12
|
-
tokens?: { input: number; output: number };
|
|
13
|
-
contextPercent?: number;
|
|
14
|
-
status: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function SessionHeader(props: SessionHeaderProps) {
|
|
18
|
-
const { theme } = useTheme();
|
|
19
|
-
const t = theme;
|
|
20
|
-
|
|
21
|
-
const inTok = props.tokens ? props.tokens.input.toLocaleString() : "0";
|
|
22
|
-
const outTok = props.tokens ? props.tokens.output.toLocaleString() : "0";
|
|
23
|
-
const pct = props.contextPercent ? Math.round(props.contextPercent) : 0;
|
|
24
|
-
const pctColor = pct > 75 ? t.error : pct > 50 ? t.warning : t.success;
|
|
25
|
-
const statusColor = props.status === "Error" ? t.error
|
|
26
|
-
: props.status === "Processing..." ? t.warning : t.success;
|
|
27
|
-
|
|
28
|
-
const left = ` ◆ ${props.title || "Session"} │ ${props.provider}/${props.model} │ ${inTok}→${outTok} tokens`;
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<box height={1} flexDirection="row" backgroundColor={t.bgSubtle}>
|
|
32
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>{left}</text>
|
|
33
|
-
<text fg={t.border}>{" │ "}</text>
|
|
34
|
-
<text fg={pctColor}>{`${pct}%`}</text>
|
|
35
|
-
<box flexGrow={1} />
|
|
36
|
-
<text fg={statusColor}>{`${props.status} `}</text>
|
|
37
|
-
</box>
|
|
38
|
-
);
|
|
39
|
-
}
|