obsidian-catui-acp 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.
@@ -0,0 +1,124 @@
1
+ import { App, PluginSettingTab, Setting } from "obsidian";
2
+ import type CatuiAcpPlugin from "./main";
3
+
4
+ export interface CatuiAcpSettings {
5
+ command: string;
6
+ args: string;
7
+ personality: string;
8
+ defaultMode: "ask" | "read-only" | "bypass";
9
+ autoStart: boolean;
10
+ includeActiveNote: boolean;
11
+ includeSelection: boolean;
12
+ protectRawWrites: boolean;
13
+ }
14
+
15
+ export const DEFAULT_SETTINGS: CatuiAcpSettings = {
16
+ command: "catui",
17
+ args: "--acp",
18
+ personality: "vex",
19
+ defaultMode: "ask",
20
+ autoStart: true,
21
+ includeActiveNote: true,
22
+ includeSelection: true,
23
+ protectRawWrites: true,
24
+ };
25
+
26
+ export function splitArgs(value: string): string[] {
27
+ return value.trim().length === 0 ? [] : value.trim().split(/\s+/);
28
+ }
29
+
30
+ export class CatuiAcpSettingTab extends PluginSettingTab {
31
+ constructor(app: App, private readonly plugin: CatuiAcpPlugin) {
32
+ super(app, plugin);
33
+ }
34
+
35
+ display(): void {
36
+ const { containerEl } = this;
37
+ containerEl.empty();
38
+
39
+ new Setting(containerEl)
40
+ .setName("CatUI command")
41
+ .setDesc("Executable used to start CatUI.")
42
+ .addText((text) =>
43
+ text.setValue(this.plugin.settings.command).onChange(async (value) => {
44
+ this.plugin.settings.command = value.trim() || DEFAULT_SETTINGS.command;
45
+ await this.plugin.saveSettings();
46
+ }),
47
+ );
48
+
49
+ new Setting(containerEl)
50
+ .setName("CatUI arguments")
51
+ .setDesc("Arguments passed to the command.")
52
+ .addText((text) =>
53
+ text.setValue(this.plugin.settings.args).onChange(async (value) => {
54
+ this.plugin.settings.args = value;
55
+ await this.plugin.saveSettings();
56
+ }),
57
+ );
58
+
59
+ new Setting(containerEl)
60
+ .setName("CatUI personality")
61
+ .setDesc("Default personality sent with each prompt.")
62
+ .addText((text) =>
63
+ text.setValue(this.plugin.settings.personality).onChange(async (value) => {
64
+ this.plugin.settings.personality = value.trim() || DEFAULT_SETTINGS.personality;
65
+ await this.plugin.saveSettings();
66
+ }),
67
+ );
68
+
69
+ new Setting(containerEl)
70
+ .setName("Default mode")
71
+ .setDesc("ACP mode for new sessions.")
72
+ .addDropdown((dropdown) =>
73
+ dropdown
74
+ .addOption("ask", "Ask before write")
75
+ .addOption("read-only", "Read-only")
76
+ .addOption("bypass", "Bypass permissions")
77
+ .setValue(this.plugin.settings.defaultMode)
78
+ .onChange(async (value) => {
79
+ this.plugin.settings.defaultMode = value as CatuiAcpSettings["defaultMode"];
80
+ await this.plugin.saveSettings();
81
+ }),
82
+ );
83
+
84
+ new Setting(containerEl)
85
+ .setName("Auto-start")
86
+ .setDesc("Start CatUI when the sidebar opens.")
87
+ .addToggle((toggle) =>
88
+ toggle.setValue(this.plugin.settings.autoStart).onChange(async (value) => {
89
+ this.plugin.settings.autoStart = value;
90
+ await this.plugin.saveSettings();
91
+ }),
92
+ );
93
+
94
+ new Setting(containerEl)
95
+ .setName("Include active note")
96
+ .setDesc("Send the active note path with each prompt.")
97
+ .addToggle((toggle) =>
98
+ toggle.setValue(this.plugin.settings.includeActiveNote).onChange(async (value) => {
99
+ this.plugin.settings.includeActiveNote = value;
100
+ await this.plugin.saveSettings();
101
+ }),
102
+ );
103
+
104
+ new Setting(containerEl)
105
+ .setName("Include selected text")
106
+ .setDesc("Send the current editor selection with each prompt.")
107
+ .addToggle((toggle) =>
108
+ toggle.setValue(this.plugin.settings.includeSelection).onChange(async (value) => {
109
+ this.plugin.settings.includeSelection = value;
110
+ await this.plugin.saveSettings();
111
+ }),
112
+ );
113
+
114
+ new Setting(containerEl)
115
+ .setName("Protect raw writes")
116
+ .setDesc("Reject write/edit permission requests that target raw sources.")
117
+ .addToggle((toggle) =>
118
+ toggle.setValue(this.plugin.settings.protectRawWrites).onChange(async (value) => {
119
+ this.plugin.settings.protectRawWrites = value;
120
+ await this.plugin.saveSettings();
121
+ }),
122
+ );
123
+ }
124
+ }
package/src/styles.css ADDED
@@ -0,0 +1,270 @@
1
+ @import "tailwindcss/theme.css" prefix(catui);
2
+
3
+ @theme prefix(catui) {
4
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
5
+ --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
6
+ --color-bg: var(--catui-background);
7
+ --color-panel: var(--catui-muted);
8
+ --color-border: var(--catui-border);
9
+ --color-hover: var(--catui-foreground-3);
10
+ --color-normal: var(--catui-foreground);
11
+ --color-muted: var(--catui-muted-foreground);
12
+ --color-faint: var(--catui-foreground-35);
13
+ --color-accent: var(--catui-accent);
14
+ --color-accent-hover: color-mix(in oklch, var(--catui-accent) 88%, var(--catui-foreground));
15
+ --color-on-accent: white;
16
+ --color-success: var(--catui-success);
17
+ --color-error: var(--catui-destructive);
18
+ --color-error-bg: color-mix(in oklch, var(--catui-destructive) 8%, transparent);
19
+ --color-info: var(--catui-info);
20
+ --color-info-bg: color-mix(in oklch, var(--catui-info) 5%, transparent);
21
+ --color-foreground-subtle: var(--catui-foreground-3);
22
+ --color-foreground-soft: var(--catui-foreground-5);
23
+ --color-foreground-15: var(--catui-foreground-15);
24
+ --color-foreground-20: var(--catui-foreground-20);
25
+ --color-user-bubble: var(--catui-user-message-bubble);
26
+ --radius-catui-xs: 4px;
27
+ --radius-catui-sm: 6px;
28
+ --radius-catui-md: 8px;
29
+ --shadow-catui-minimal: var(--catui-shadow-minimal);
30
+ --shadow-catui-middle: var(--catui-shadow-middle);
31
+ --shadow-catui-focused: var(--catui-shadow-focused);
32
+ --shadow-catui-tinted: var(--catui-shadow-tinted);
33
+ }
34
+
35
+ @source "./src";
36
+ @tailwind utilities;
37
+
38
+ .catui-acp-view {
39
+ --catui-background: oklch(0.98 0.003 265);
40
+ --catui-foreground: oklch(0.185 0.01 270);
41
+ --catui-foreground-rgb: 38, 36, 42;
42
+ --catui-foreground-3: color-mix(in oklch, var(--catui-foreground) 3%, transparent);
43
+ --catui-foreground-5: color-mix(in oklch, var(--catui-foreground) 5%, transparent);
44
+ --catui-foreground-15: color-mix(in oklch, var(--catui-foreground) 15%, transparent);
45
+ --catui-foreground-20: color-mix(in oklch, var(--catui-foreground) 20%, transparent);
46
+ --catui-foreground-35: color-mix(in oklch, var(--catui-foreground) 35%, transparent);
47
+ --catui-muted: var(--catui-foreground-5);
48
+ --catui-muted-foreground: color-mix(in oklch, var(--catui-foreground) 50%, transparent);
49
+ --catui-border: var(--catui-foreground-5);
50
+ --catui-user-message-bubble: color-mix(in oklch, var(--catui-foreground) 5%, transparent);
51
+ --catui-accent: oklch(0.62 0.13 293);
52
+ --catui-success: oklch(0.55 0.17 145);
53
+ --catui-info: oklch(0.75 0.16 70);
54
+ --catui-info-rgb: 180, 120, 40;
55
+ --catui-destructive: oklch(0.58 0.24 28);
56
+ --catui-shadow-blur-opacity: 0.06;
57
+ --catui-shadow-minimal:
58
+ rgba(0, 0, 0, 0) 0 0 0 0,
59
+ rgba(0, 0, 0, 0) 0 0 0 0,
60
+ rgba(var(--catui-foreground-rgb), 0.06) 0 0 0 1px,
61
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -0.5px,
62
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px;
63
+ --catui-shadow-middle:
64
+ rgba(0, 0, 0, 0) 0 0 0 0,
65
+ rgba(0, 0, 0, 0) 0 0 0 0,
66
+ rgba(var(--catui-foreground-rgb), 0.06) 0 0 0 1px,
67
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -0.5px,
68
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px,
69
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 6px 6px -3px;
70
+ --catui-shadow-focused:
71
+ 0 0 0 1px color-mix(in oklch, var(--catui-accent) 45%, transparent),
72
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -0.5px,
73
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px,
74
+ rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 6px 6px -3px;
75
+ --catui-shadow-tinted:
76
+ rgba(var(--catui-info-rgb), 0.12) 0 0 0 1px,
77
+ rgba(var(--catui-info-rgb), 0.08) 0 1px 1px -0.5px,
78
+ rgba(var(--catui-info-rgb), 0.06) 0 3px 3px -1.5px;
79
+ background: var(--catui-background);
80
+ color: var(--catui-foreground);
81
+ font-size: 15px;
82
+ height: 100%;
83
+ min-height: 0;
84
+ }
85
+
86
+ .theme-dark .catui-acp-view,
87
+ body.theme-dark .catui-acp-view {
88
+ --catui-background: oklch(0.145 0.015 270);
89
+ --catui-foreground: oklch(0.95 0.01 270);
90
+ --catui-foreground-rgb: 237, 236, 240;
91
+ --catui-accent: oklch(0.65 0.22 293);
92
+ --catui-success: oklch(0.60 0.17 145);
93
+ --catui-info: oklch(0.78 0.14 70);
94
+ --catui-info-rgb: 210, 155, 55;
95
+ --catui-destructive: oklch(0.65 0.22 28);
96
+ --catui-shadow-blur-opacity: 0.12;
97
+ }
98
+
99
+ :where(.catui-acp-view) :where(*, *::before, *::after) {
100
+ box-sizing: border-box;
101
+ letter-spacing: 0;
102
+ }
103
+
104
+ :where(.catui-acp-view) :where(button, textarea, select) {
105
+ appearance: none;
106
+ font: inherit;
107
+ letter-spacing: 0;
108
+ margin: 0;
109
+ }
110
+
111
+ :where(.catui-acp-view) :where(button) {
112
+ min-width: 0;
113
+ padding: 0;
114
+ }
115
+
116
+ :where(.catui-acp-view) :where(textarea, select) {
117
+ box-shadow: none;
118
+ outline: 0;
119
+ }
120
+
121
+ .catui-acp-view textarea,
122
+ .catui-acp-view textarea:hover,
123
+ .catui-acp-view textarea:active,
124
+ .catui-acp-view textarea:focus,
125
+ .catui-acp-view textarea:focus-visible {
126
+ background: transparent !important;
127
+ border: 0 !important;
128
+ box-shadow: none !important;
129
+ outline: 0 !important;
130
+ }
131
+
132
+ .catui-reset-button,
133
+ .catui-reset-button:hover {
134
+ box-shadow: none;
135
+ }
136
+
137
+ .catui-acp-view svg.lucide {
138
+ color: inherit !important;
139
+ display: block !important;
140
+ opacity: 1 !important;
141
+ overflow: visible;
142
+ stroke: currentColor !important;
143
+ stroke-linecap: round;
144
+ stroke-linejoin: round;
145
+ stroke-width: 2;
146
+ visibility: visible !important;
147
+ }
148
+
149
+ .catui-conversation {
150
+ mask-image: linear-gradient(to bottom, transparent 0%, black 32px, black calc(100% - 32px), transparent 100%);
151
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 32px, black calc(100% - 32px), transparent 100%);
152
+ }
153
+
154
+ .catui-input-zone {
155
+ padding-bottom: calc(12px + var(--status-bar-scroll-padding, 30px));
156
+ }
157
+
158
+ .catui-acp-view .catui-session-menu {
159
+ background: var(--catui-background) !important;
160
+ border: 1px solid var(--catui-foreground-15) !important;
161
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28) !important;
162
+ font-family: var(--catui-font-sans) !important;
163
+ }
164
+
165
+ .catui-acp-view .catui-session-menu-header {
166
+ min-height: 26px;
167
+ }
168
+
169
+ .catui-acp-view .catui-session-menu-heading,
170
+ .catui-acp-view .catui-session-menu-count {
171
+ font-family: var(--catui-font-sans) !important;
172
+ line-height: 14px !important;
173
+ }
174
+
175
+ .catui-acp-view .catui-session-menu-heading {
176
+ font-size: 11px !important;
177
+ }
178
+
179
+ .catui-acp-view .catui-session-menu-count {
180
+ font-size: 10px !important;
181
+ }
182
+
183
+ .catui-acp-view button.catui-session-item,
184
+ .catui-acp-view button.catui-session-item:hover,
185
+ .catui-acp-view button.catui-session-item:focus,
186
+ .catui-acp-view button.catui-session-item:active {
187
+ background: transparent !important;
188
+ border: 0 !important;
189
+ box-shadow: none !important;
190
+ font-family: var(--catui-font-sans) !important;
191
+ height: auto !important;
192
+ min-height: 38px;
193
+ }
194
+
195
+ .catui-acp-view button.catui-session-item:hover,
196
+ .catui-acp-view button.catui-session-item:focus-visible,
197
+ .catui-acp-view button.catui-session-item[aria-current="true"] {
198
+ background: var(--catui-foreground-5) !important;
199
+ }
200
+
201
+ .catui-acp-view .catui-session-item-title {
202
+ font-family: var(--catui-font-sans) !important;
203
+ font-size: 12px !important;
204
+ font-weight: 500 !important;
205
+ line-height: 16px !important;
206
+ }
207
+
208
+ .catui-acp-view .catui-session-item-time {
209
+ font-family: var(--catui-font-sans) !important;
210
+ font-size: 10px !important;
211
+ font-weight: 400 !important;
212
+ line-height: 13px !important;
213
+ }
214
+
215
+ .catui-session-menu > summary::-webkit-details-marker,
216
+ .catui-tool-part > summary::-webkit-details-marker {
217
+ display: none;
218
+ }
219
+
220
+ .catui-activity-list {
221
+ max-height: 0;
222
+ opacity: 0;
223
+ overflow: hidden;
224
+ transition: max-height 250ms cubic-bezier(0.4, 0, 0.2, 1), opacity 150ms ease;
225
+ }
226
+
227
+ .catui-activity-list-open {
228
+ max-height: 420px;
229
+ opacity: 1;
230
+ }
231
+
232
+ .catui-activity-list-open .catui-tool-part {
233
+ animation: catui-activity-enter 300ms ease-out both;
234
+ }
235
+
236
+ .catui-activity-list-open .catui-tool-part:nth-child(2) { animation-delay: 30ms; }
237
+ .catui-activity-list-open .catui-tool-part:nth-child(3) { animation-delay: 60ms; }
238
+ .catui-activity-list-open .catui-tool-part:nth-child(4) { animation-delay: 90ms; }
239
+ .catui-activity-list-open .catui-tool-part:nth-child(5) { animation-delay: 120ms; }
240
+
241
+ .catui-thinking-label {
242
+ animation: catui-crossfade 400ms ease-in-out;
243
+ }
244
+
245
+ @keyframes catui-activity-enter {
246
+ from { opacity: 0; transform: translateX(-8px); }
247
+ to { opacity: 1; transform: translateX(0); }
248
+ }
249
+
250
+ @keyframes catui-crossfade {
251
+ from { opacity: 0; }
252
+ to { opacity: 1; }
253
+ }
254
+
255
+ .catui-acp-markdown > :first-child { margin-top: 0; }
256
+ .catui-acp-markdown > :last-child { margin-bottom: 0; }
257
+ .catui-acp-markdown :where(h1, h2, h3, h4, h5, h6, p, ul, ol, blockquote, pre, table) { margin-top: 0; margin-bottom: 0; }
258
+ .catui-acp-markdown pre { white-space: pre; }
259
+ .catui-acp-markdown code { white-space: pre-wrap; }
260
+ .catui-acp-markdown pre code { white-space: pre; }
261
+
262
+ @media (prefers-reduced-motion: reduce) {
263
+ .catui-acp-view *,
264
+ .catui-acp-view *::before,
265
+ .catui-acp-view *::after {
266
+ animation-duration: 0.01ms !important;
267
+ animation-iteration-count: 1 !important;
268
+ transition-duration: 0.01ms !important;
269
+ }
270
+ }
@@ -0,0 +1,38 @@
1
+ import { ItemView, WorkspaceLeaf } from "obsidian";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import type CatuiAcpPlugin from "../main";
4
+ import { ChatApp } from "../react/ChatApp";
5
+
6
+ export const CATUI_ACP_VIEW_TYPE = "catui-acp-chat";
7
+
8
+ export class CatuiAcpView extends ItemView {
9
+ private root: Root | null = null;
10
+
11
+ constructor(
12
+ leaf: WorkspaceLeaf,
13
+ private readonly plugin: CatuiAcpPlugin,
14
+ ) {
15
+ super(leaf);
16
+ }
17
+
18
+ getViewType(): string {
19
+ return CATUI_ACP_VIEW_TYPE;
20
+ }
21
+
22
+ getDisplayText(): string {
23
+ return "CatUI ACP";
24
+ }
25
+
26
+ async onOpen(): Promise<void> {
27
+ this.containerEl.empty();
28
+ const mount = this.containerEl.createDiv({ cls: "catui-acp-view" });
29
+ this.root = createRoot(mount);
30
+ this.root.render(<ChatApp plugin={this.plugin} />);
31
+ if (this.plugin.settings.autoStart) await this.plugin.ensureClient();
32
+ }
33
+
34
+ async onClose(): Promise<void> {
35
+ this.root?.unmount();
36
+ this.root = null;
37
+ }
38
+ }
package/styles.css ADDED
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial}}}:root,:host{--catui-font-sans:system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--catui-font-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;--catui-spacing:.25rem;--catui-font-weight-medium:500;--catui-font-weight-semibold:600;--catui-animate-spin:spin 1s linear infinite;--catui-default-transition-duration:.15s;--catui-default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--catui-color-bg:var(--catui-background);--catui-color-panel:var(--catui-muted);--catui-color-border:var(--catui-border);--catui-color-hover:var(--catui-foreground-3);--catui-color-normal:var(--catui-foreground);--catui-color-muted:var(--catui-muted-foreground);--catui-color-faint:var(--catui-foreground-35);--catui-color-accent:var(--catui-accent);--catui-color-accent-hover:var(--catui-accent)}@supports (color:color-mix(in lab, red, red)){:root,:host{--catui-color-accent-hover:color-mix(in oklch, var(--catui-accent) 88%, var(--catui-foreground))}}:root,:host{--catui-color-on-accent:white;--catui-color-success:var(--catui-success);--catui-color-error:var(--catui-destructive);--catui-color-error-bg:var(--catui-destructive)}@supports (color:color-mix(in lab, red, red)){:root,:host{--catui-color-error-bg:color-mix(in oklch, var(--catui-destructive) 8%, transparent)}}:root,:host{--catui-color-info:var(--catui-info);--catui-color-info-bg:var(--catui-info)}@supports (color:color-mix(in lab, red, red)){:root,:host{--catui-color-info-bg:color-mix(in oklch, var(--catui-info) 5%, transparent)}}:root,:host{--catui-color-foreground-subtle:var(--catui-foreground-3);--catui-color-foreground-soft:var(--catui-foreground-5);--catui-color-foreground-15:var(--catui-foreground-15);--catui-color-foreground-20:var(--catui-foreground-20);--catui-color-user-bubble:var(--catui-user-message-bubble);--catui-radius-catui-xs:4px;--catui-radius-catui-sm:6px;--catui-radius-catui-md:8px}.catui\:pointer-events-none{pointer-events:none}.catui\:absolute{position:absolute}.catui\:relative{position:relative}.catui\:inset-0{inset:0}.catui\:bottom-full{bottom:100%}.catui\:left-0{left:0}.catui\:z-50{z-index:50}.catui\:m-0{margin:0}.catui\:mx-2{margin-inline:calc(var(--catui-spacing) * 2)}.catui\:mx-auto{margin-inline:auto}.catui\:my-0\.5{margin-block:calc(var(--catui-spacing) * .5)}.catui\:my-1{margin-block:var(--catui-spacing)}.catui\:my-1\.5{margin-block:calc(var(--catui-spacing) * 1.5)}.catui\:my-2{margin-block:calc(var(--catui-spacing) * 2)}.catui\:mt-0\.5{margin-top:calc(var(--catui-spacing) * .5)}.catui\:mt-1{margin-top:var(--catui-spacing)}.catui\:mt-1\.5{margin-top:calc(var(--catui-spacing) * 1.5)}.catui\:mt-2{margin-top:calc(var(--catui-spacing) * 2)}.catui\:mr-1{margin-right:var(--catui-spacing)}.catui\:-mb-1{margin-bottom:calc(var(--catui-spacing) * -1)}.catui\:mb-1{margin-bottom:var(--catui-spacing)}.catui\:mb-2{margin-bottom:calc(var(--catui-spacing) * 2)}.catui\:-ml-0\.5{margin-left:calc(var(--catui-spacing) * -.5)}.catui\:ml-1\.5{margin-left:calc(var(--catui-spacing) * 1.5)}.catui\:ml-2{margin-left:calc(var(--catui-spacing) * 2)}.catui\:ml-5{margin-left:calc(var(--catui-spacing) * 5)}.catui\:ml-\[13px\]{margin-left:13px}.catui\:block{display:block}.catui\:flex{display:flex}.catui\:grid{display:grid}.catui\:inline-flex{display:inline-flex}.catui\:size-3{width:calc(var(--catui-spacing) * 3);height:calc(var(--catui-spacing) * 3)}.catui\:size-4{width:calc(var(--catui-spacing) * 4);height:calc(var(--catui-spacing) * 4)}.catui\:size-6{width:calc(var(--catui-spacing) * 6);height:calc(var(--catui-spacing) * 6)}.catui\:size-7{width:calc(var(--catui-spacing) * 7);height:calc(var(--catui-spacing) * 7)}.catui\:h-6{height:calc(var(--catui-spacing) * 6)}.catui\:h-8{height:calc(var(--catui-spacing) * 8)}.catui\:h-full{height:100%}.catui\:max-h-24{max-height:calc(var(--catui-spacing) * 24)}.catui\:max-h-\[180px\]{max-height:180px}.catui\:max-h-\[220px\]{max-height:220px}.catui\:max-h-\[260px\]{max-height:260px}.catui\:max-h-\[280px\]{max-height:280px}.catui\:max-h-\[360px\]{max-height:360px}.catui\:max-h-\[560px\]{max-height:560px}.catui\:min-h-0{min-height:0}.catui\:min-h-6{min-height:calc(var(--catui-spacing) * 6)}.catui\:min-h-\[88px\]{min-height:88px}.catui\:w-\[280px\]{width:280px}.catui\:w-full{width:100%}.catui\:max-w-\[80\%\]{max-width:80%}.catui\:max-w-\[148px\]{max-width:148px}.catui\:max-w-\[840px\]{max-width:840px}.catui\:max-w-\[calc\(100vw-32px\)\]{max-width:calc(100vw - 32px)}.catui\:min-w-0{min-width:0}.catui\:flex-1{flex:1}.catui\:shrink{flex-shrink:1}.catui\:shrink-0{flex-shrink:0}.catui\:border-collapse{border-collapse:collapse}.catui\:rotate-90{rotate:90deg}.catui\:rotate-180{rotate:180deg}.catui\:animate-spin{animation:var(--catui-animate-spin)}.catui\:cursor-default{cursor:default}.catui\:cursor-pointer{cursor:pointer}.catui\:resize-none{resize:none}.catui\:list-decimal{list-style-type:decimal}.catui\:list-disc{list-style-type:disc}.catui\:list-none{list-style-type:none}.catui\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.catui\:flex-col{flex-direction:column}.catui\:items-center{align-items:center}.catui\:items-end{align-items:flex-end}.catui\:justify-between{justify-content:space-between}.catui\:justify-center{justify-content:center}.catui\:gap-1{gap:var(--catui-spacing)}.catui\:gap-1\.5{gap:calc(var(--catui-spacing) * 1.5)}.catui\:gap-2{gap:calc(var(--catui-spacing) * 2)}.catui\:gap-3{gap:calc(var(--catui-spacing) * 3)}:where(.catui\:space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--catui-spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--catui-spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.catui\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--catui-spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--catui-spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.catui\:divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.catui\:divide-border>:not(:last-child)){border-color:var(--catui-color-border)}.catui\:truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.catui\:overflow-auto{overflow:auto}.catui\:overflow-hidden{overflow:hidden}.catui\:overflow-visible{overflow:visible}.catui\:overflow-x-auto{overflow-x:auto}.catui\:overflow-y-auto{overflow-y:auto}.catui\:rounded-\[16px\]{border-radius:16px}.catui\:rounded-catui-md{border-radius:var(--catui-radius-catui-md)}.catui\:rounded-catui-sm{border-radius:var(--catui-radius-catui-sm)}.catui\:rounded-catui-xs{border-radius:var(--catui-radius-catui-xs)}.catui\:rounded-full{border-radius:3.40282e38px}.catui\:border{border-style:var(--tw-border-style);border-width:1px}.catui\:border-0{border-style:var(--tw-border-style);border-width:0}.catui\:border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.catui\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.catui\:border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.catui\:border-accent{border-color:var(--catui-color-accent)}.catui\:border-border{border-color:var(--catui-color-border)}.catui\:border-error\/30{border-color:var(--catui-color-error)}@supports (color:color-mix(in lab, red, red)){.catui\:border-error\/30{border-color:color-mix(in oklab, var(--catui-color-error) 30%, transparent)}}.catui\:border-info\/30{border-color:var(--catui-color-info)}@supports (color:color-mix(in lab, red, red)){.catui\:border-info\/30{border-color:color-mix(in oklab, var(--catui-color-info) 30%, transparent)}}.catui\:border-muted{border-color:var(--catui-color-muted)}.catui\:bg-accent{background-color:var(--catui-color-accent)}.catui\:bg-bg{background-color:var(--catui-color-bg)}.catui\:bg-error-bg{background-color:var(--catui-color-error-bg)}.catui\:bg-foreground-soft{background-color:var(--catui-color-foreground-soft)}.catui\:bg-foreground-subtle{background-color:var(--catui-color-foreground-subtle)}.catui\:bg-hover{background-color:var(--catui-color-hover)}.catui\:bg-info-bg{background-color:var(--catui-color-info-bg)}.catui\:bg-normal{background-color:var(--catui-color-normal)}.catui\:bg-panel{background-color:var(--catui-color-panel)}.catui\:bg-transparent{background-color:#0000}.catui\:bg-user-bubble{background-color:var(--catui-color-user-bubble)}.catui\:p-1\.5{padding:calc(var(--catui-spacing) * 1.5)}.catui\:p-2{padding:calc(var(--catui-spacing) * 2)}.catui\:p-2\.5{padding:calc(var(--catui-spacing) * 2.5)}.catui\:p-3{padding:calc(var(--catui-spacing) * 3)}.catui\:p-4{padding:calc(var(--catui-spacing) * 4)}.catui\:px-1{padding-inline:var(--catui-spacing)}.catui\:px-1\.5{padding-inline:calc(var(--catui-spacing) * 1.5)}.catui\:px-2{padding-inline:calc(var(--catui-spacing) * 2)}.catui\:px-2\.5{padding-inline:calc(var(--catui-spacing) * 2.5)}.catui\:px-3{padding-inline:calc(var(--catui-spacing) * 3)}.catui\:px-4{padding-inline:calc(var(--catui-spacing) * 4)}.catui\:py-0{padding-block:0}.catui\:py-0\.5{padding-block:calc(var(--catui-spacing) * .5)}.catui\:py-1{padding-block:var(--catui-spacing)}.catui\:py-1\.5{padding-block:calc(var(--catui-spacing) * 1.5)}.catui\:py-2{padding-block:calc(var(--catui-spacing) * 2)}.catui\:py-2\.5{padding-block:calc(var(--catui-spacing) * 2.5)}.catui\:py-3{padding-block:calc(var(--catui-spacing) * 3)}.catui\:py-4{padding-block:calc(var(--catui-spacing) * 4)}.catui\:pt-1{padding-top:var(--catui-spacing)}.catui\:pt-2{padding-top:calc(var(--catui-spacing) * 2)}.catui\:pt-4{padding-top:calc(var(--catui-spacing) * 4)}.catui\:pr-1\.5{padding-right:calc(var(--catui-spacing) * 1.5)}.catui\:pr-2{padding-right:calc(var(--catui-spacing) * 2)}.catui\:pr-4{padding-right:calc(var(--catui-spacing) * 4)}.catui\:pb-1{padding-bottom:var(--catui-spacing)}.catui\:pb-1\.5{padding-bottom:calc(var(--catui-spacing) * 1.5)}.catui\:pb-3{padding-bottom:calc(var(--catui-spacing) * 3)}.catui\:pl-2{padding-left:calc(var(--catui-spacing) * 2)}.catui\:pl-2\.5{padding-left:calc(var(--catui-spacing) * 2.5)}.catui\:pl-4{padding-left:calc(var(--catui-spacing) * 4)}.catui\:pl-5{padding-left:calc(var(--catui-spacing) * 5)}.catui\:pl-\[22px\]{padding-left:22px}.catui\:text-center{text-align:center}.catui\:text-left{text-align:left}.catui\:font-mono{font-family:var(--catui-font-mono)}.catui\:font-sans{font-family:var(--catui-font-sans)}.catui\:text-\[10px\]{font-size:10px}.catui\:text-\[11px\]{font-size:11px}.catui\:text-\[12px\]{font-size:12px}.catui\:text-\[13px\]{font-size:13px}.catui\:text-\[14px\]{font-size:14px}.catui\:text-\[15px\]{font-size:15px}.catui\:leading-4{--tw-leading:calc(var(--catui-spacing) * 4);line-height:calc(var(--catui-spacing) * 4)}.catui\:leading-5{--tw-leading:calc(var(--catui-spacing) * 5);line-height:calc(var(--catui-spacing) * 5)}.catui\:leading-6{--tw-leading:calc(var(--catui-spacing) * 6);line-height:calc(var(--catui-spacing) * 6)}.catui\:leading-\[18px\]{--tw-leading:18px;line-height:18px}.catui\:leading-none{--tw-leading:1;line-height:1}.catui\:font-medium{--tw-font-weight:var(--catui-font-weight-medium);font-weight:var(--catui-font-weight-medium)}.catui\:font-semibold{--tw-font-weight:var(--catui-font-weight-semibold);font-weight:var(--catui-font-weight-semibold)}.catui\:break-words{overflow-wrap:break-word}.catui\:whitespace-pre{white-space:pre}.catui\:whitespace-pre-wrap{white-space:pre-wrap}.catui\:text-accent{color:var(--catui-color-accent)}.catui\:text-bg{color:var(--catui-color-bg)}.catui\:text-error{color:var(--catui-color-error)}.catui\:text-faint{color:var(--catui-color-faint)}.catui\:text-info{color:var(--catui-color-info)}.catui\:text-muted{color:var(--catui-color-muted)}.catui\:text-normal{color:var(--catui-color-normal)}.catui\:text-on-accent{color:var(--catui-color-on-accent)}.catui\:text-success{color:var(--catui-color-success)}.catui\:uppercase{text-transform:uppercase}.catui\:italic{font-style:italic}.catui\:tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.catui\:line-through{text-decoration-line:line-through}.catui\:underline{text-decoration-line:underline}.catui\:decoration-accent\/40{-webkit-text-decoration-color:var(--catui-color-accent);-webkit-text-decoration-color:var(--catui-color-accent);text-decoration-color:var(--catui-color-accent)}@supports (color:color-mix(in lab, red, red)){.catui\:decoration-accent\/40{-webkit-text-decoration-color:color-mix(in oklab, var(--catui-color-accent) 40%, transparent);-webkit-text-decoration-color:color-mix(in oklab, var(--catui-color-accent) 40%, transparent);text-decoration-color:color-mix(in oklab, var(--catui-color-accent) 40%, transparent)}}.catui\:underline-offset-2{text-underline-offset:2px}.catui\:accent-accent{accent-color:var(--catui-color-accent)}.catui\:shadow-catui-middle{--tw-shadow:var(--catui-shadow-middle);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.catui\:shadow-catui-minimal{--tw-shadow:var(--catui-shadow-minimal);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.catui\:shadow-catui-tinted{--tw-shadow:var(--catui-shadow-tinted);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.catui\:transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--catui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--catui-default-transition-duration))}.catui\:transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--catui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--catui-default-transition-duration))}.catui\:transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--catui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--catui-default-transition-duration))}.catui\:transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--catui-default-transition-timing-function));transition-duration:var(--tw-duration,var(--catui-default-transition-duration))}.catui\:duration-150{--tw-duration:.15s;transition-duration:.15s}.catui\:outline-none{--tw-outline-style:none;outline-style:none}.catui\:select-none{-webkit-user-select:none;user-select:none}.catui\:select-text{-webkit-user-select:text;user-select:text}.catui\:placeholder\:text-faint::placeholder{color:var(--catui-color-faint)}.catui\:last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.catui\:focus-within\:shadow-catui-focused:focus-within{--tw-shadow:var(--catui-shadow-focused);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.catui\:hover\:bg-accent-hover:hover{background-color:var(--catui-color-accent-hover)}.catui\:hover\:bg-error-bg:hover{background-color:var(--catui-color-error-bg)}.catui\:hover\:bg-foreground-15:hover{background-color:var(--catui-color-foreground-15)}.catui\:hover\:bg-hover:hover{background-color:var(--catui-color-hover)}.catui\:hover\:bg-info\/20:hover{background-color:var(--catui-color-info)}@supports (color:color-mix(in lab, red, red)){.catui\:hover\:bg-info\/20:hover{background-color:color-mix(in oklab, var(--catui-color-info) 20%, transparent)}}.catui\:hover\:text-error:hover{color:var(--catui-color-error)}.catui\:hover\:text-normal:hover{color:var(--catui-color-normal)}.catui\:hover\:text-on-accent:hover{color:var(--catui-color-on-accent)}.catui\:hover\:decoration-accent:hover{-webkit-text-decoration-color:var(--catui-color-accent);-webkit-text-decoration-color:var(--catui-color-accent);text-decoration-color:var(--catui-color-accent)}.catui\:hover\:opacity-80:hover{opacity:.8}}.catui\:focus-visible\:border-accent:focus-visible{border-color:var(--catui-color-accent)}.catui\:focus-visible\:bg-hover:focus-visible{background-color:var(--catui-color-hover)}.catui\:focus-visible\:decoration-accent:focus-visible{-webkit-text-decoration-color:var(--catui-color-accent);-webkit-text-decoration-color:var(--catui-color-accent);text-decoration-color:var(--catui-color-accent)}.catui\:focus-visible\:shadow-\[0_0_0_1px_var\(--interactive-accent\)\]:focus-visible{--tw-shadow:0 0 0 1px var(--tw-shadow-color,var(--interactive-accent));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.catui\:focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.catui\:focus-visible\:ring-accent:focus-visible{--tw-ring-color:var(--catui-color-accent)}.catui\:focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.catui\:active\:bg-foreground-20:active{background-color:var(--catui-color-foreground-20)}.catui\:disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.catui\:disabled\:opacity-30:disabled{opacity:.3}.catui\:disabled\:opacity-40:disabled{opacity:.4}.catui\:disabled\:opacity-50:disabled{opacity:.5}.catui-acp-view{--catui-background:oklch(98% .003 265);--catui-foreground:oklch(18.5% .01 270);--catui-foreground-rgb:38, 36, 42;--catui-foreground-3:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-foreground-3:color-mix(in oklch, var(--catui-foreground) 3%, transparent)}}.catui-acp-view{--catui-foreground-5:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-foreground-5:color-mix(in oklch, var(--catui-foreground) 5%, transparent)}}.catui-acp-view{--catui-foreground-15:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-foreground-15:color-mix(in oklch, var(--catui-foreground) 15%, transparent)}}.catui-acp-view{--catui-foreground-20:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-foreground-20:color-mix(in oklch, var(--catui-foreground) 20%, transparent)}}.catui-acp-view{--catui-foreground-35:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-foreground-35:color-mix(in oklch, var(--catui-foreground) 35%, transparent)}}.catui-acp-view{--catui-muted:var(--catui-foreground-5);--catui-muted-foreground:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-muted-foreground:color-mix(in oklch, var(--catui-foreground) 50%, transparent)}}.catui-acp-view{--catui-border:var(--catui-foreground-5);--catui-user-message-bubble:var(--catui-foreground)}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-user-message-bubble:color-mix(in oklch, var(--catui-foreground) 5%, transparent)}}.catui-acp-view{--catui-accent:oklch(62% .13 293);--catui-success:oklch(55% .17 145);--catui-info:oklch(75% .16 70);--catui-info-rgb:180, 120, 40;--catui-destructive:oklch(58% .24 28);--catui-shadow-blur-opacity:.06;--catui-shadow-minimal:#0000 0 0 0 0, #0000 0 0 0 0, rgba(var(--catui-foreground-rgb), .06) 0 0 0 1px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px;--catui-shadow-middle:#0000 0 0 0 0, #0000 0 0 0 0, rgba(var(--catui-foreground-rgb), .06) 0 0 0 1px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 6px 6px -3px;--catui-shadow-focused:0 0 0 1px var(--catui-accent), rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 6px 6px -3px}@supports (color:color-mix(in lab, red, red)){.catui-acp-view{--catui-shadow-focused:0 0 0 1px color-mix(in oklch, var(--catui-accent) 45%, transparent), rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 1px 1px -.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 3px 3px -1.5px, rgba(0, 0, 0, var(--catui-shadow-blur-opacity)) 0 6px 6px -3px}}.catui-acp-view{--catui-shadow-tinted:rgba(var(--catui-info-rgb), .12) 0 0 0 1px, rgba(var(--catui-info-rgb), .08) 0 1px 1px -.5px, rgba(var(--catui-info-rgb), .06) 0 3px 3px -1.5px;background:var(--catui-background);color:var(--catui-foreground);height:100%;min-height:0;font-size:15px}.theme-dark .catui-acp-view,body.theme-dark .catui-acp-view{--catui-background:oklch(14.5% .015 270);--catui-foreground:oklch(95% .01 270);--catui-foreground-rgb:237, 236, 240;--catui-accent:oklch(65% .22 293);--catui-success:oklch(60% .17 145);--catui-info:oklch(78% .14 70);--catui-info-rgb:210, 155, 55;--catui-destructive:oklch(65% .22 28);--catui-shadow-blur-opacity:.12}:where(.catui-acp-view) :where(*){box-sizing:border-box;letter-spacing:0}:where(.catui-acp-view) :where(button,textarea,select){appearance:none;font:inherit;letter-spacing:0;margin:0}:where(.catui-acp-view) :where(button){min-width:0;padding:0}:where(.catui-acp-view) :where(textarea,select){box-shadow:none;outline:0}.catui-acp-view textarea,.catui-acp-view textarea:hover,.catui-acp-view textarea:active,.catui-acp-view textarea:focus,.catui-acp-view textarea:focus-visible{box-shadow:none!important;background:0 0!important;border:0!important;outline:0!important}.catui-reset-button,.catui-reset-button:hover{box-shadow:none}.catui-acp-view svg.lucide{stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;overflow:visible;color:inherit!important;opacity:1!important;stroke:currentColor!important;visibility:visible!important;display:block!important}.catui-conversation{-webkit-mask-image:linear-gradient(#0000 0%,#000 32px calc(100% - 32px),#0000 100%);mask-image:linear-gradient(#0000 0%,#000 32px calc(100% - 32px),#0000 100%)}.catui-input-zone{padding-bottom:calc(12px + var(--status-bar-scroll-padding,30px))}.catui-acp-view .catui-session-menu{background:var(--catui-background)!important;border:1px solid var(--catui-foreground-15)!important;font-family:var(--catui-font-sans)!important;box-shadow:0 12px 32px #00000047!important}.catui-acp-view .catui-session-menu-header{min-height:26px}.catui-acp-view .catui-session-menu-heading,.catui-acp-view .catui-session-menu-count{font-family:var(--catui-font-sans)!important;line-height:14px!important}.catui-acp-view .catui-session-menu-heading{font-size:11px!important}.catui-acp-view .catui-session-menu-count{font-size:10px!important}.catui-acp-view button.catui-session-item,.catui-acp-view button.catui-session-item:hover,.catui-acp-view button.catui-session-item:focus,.catui-acp-view button.catui-session-item:active{min-height:38px;box-shadow:none!important;font-family:var(--catui-font-sans)!important;background:0 0!important;border:0!important;height:auto!important}.catui-acp-view button.catui-session-item:hover,.catui-acp-view button.catui-session-item:focus-visible,.catui-acp-view button.catui-session-item[aria-current=true]{background:var(--catui-foreground-5)!important}.catui-acp-view .catui-session-item-title{font-family:var(--catui-font-sans)!important;font-size:12px!important;font-weight:500!important;line-height:16px!important}.catui-acp-view .catui-session-item-time{font-family:var(--catui-font-sans)!important;font-size:10px!important;font-weight:400!important;line-height:13px!important}.catui-session-menu>summary::-webkit-details-marker{display:none}.catui-tool-part>summary::-webkit-details-marker{display:none}.catui-activity-list{opacity:0;max-height:0;transition:max-height .25s cubic-bezier(.4,0,.2,1),opacity .15s;overflow:hidden}.catui-activity-list-open{opacity:1;max-height:420px}.catui-activity-list-open .catui-tool-part{animation:.3s ease-out both catui-activity-enter}.catui-activity-list-open .catui-tool-part:nth-child(2){animation-delay:30ms}.catui-activity-list-open .catui-tool-part:nth-child(3){animation-delay:60ms}.catui-activity-list-open .catui-tool-part:nth-child(4){animation-delay:90ms}.catui-activity-list-open .catui-tool-part:nth-child(5){animation-delay:.12s}.catui-thinking-label{animation:.4s ease-in-out catui-crossfade}@keyframes catui-activity-enter{0%{opacity:0;transform:translate(-8px)}to{opacity:1;transform:translate(0)}}@keyframes catui-crossfade{0%{opacity:0}to{opacity:1}}.catui-acp-markdown>:first-child{margin-top:0}.catui-acp-markdown>:last-child{margin-bottom:0}.catui-acp-markdown :where(h1,h2,h3,h4,h5,h6,p,ul,ol,blockquote,pre,table){margin-top:0;margin-bottom:0}.catui-acp-markdown pre{white-space:pre}.catui-acp-markdown code{white-space:pre-wrap}.catui-acp-markdown pre code{white-space:pre}@media (prefers-reduced-motion:reduce){.catui-acp-view *,.catui-acp-view :before,.catui-acp-view :after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
@@ -0,0 +1,61 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { CatuiAcpClient } from "../src/acp/catui-client";
4
+
5
+ class FakePeer {
6
+ calls: Array<{ method: string; params: unknown }> = [];
7
+ async request<TResult = unknown>(method: string, params?: unknown): Promise<TResult> {
8
+ this.calls.push({ method, params });
9
+ if (method === "initialize") {
10
+ return { protocolVersion: 1, agentInfo: { name: "CatUI", version: "test" }, agentCapabilities: {} } as TResult;
11
+ }
12
+ if (method === "session/new") {
13
+ return {
14
+ sessionId: "s1",
15
+ modes: { currentModeId: "ask", availableModes: [] },
16
+ models: { currentModelId: "m1", availableModels: [] },
17
+ } as TResult;
18
+ }
19
+ if (method === "session/list") return { sessions: [] } as TResult;
20
+ if (method === "session/prompt") return { stopReason: "end_turn" } as TResult;
21
+ return {} as TResult;
22
+ }
23
+ notify(method: string, params?: unknown): void {
24
+ this.calls.push({ method, params });
25
+ }
26
+ onNotification(): void {}
27
+ onRequest(): void {}
28
+ }
29
+
30
+ test("CatuiAcpClient initializes and creates a session for the vault root", async () => {
31
+ const peer = new FakePeer();
32
+ const client = new CatuiAcpClient(peer, "/vault");
33
+
34
+ await client.initialize();
35
+ await client.newSession();
36
+
37
+ assert.equal(peer.calls[0]!.method, "initialize");
38
+ assert.deepEqual(peer.calls[1], { method: "session/new", params: { cwd: "/vault", mcpServers: [] } });
39
+ assert.equal(client.sessionId, "s1");
40
+ });
41
+
42
+ test("CatuiAcpClient sends prompt as ACP text content", async () => {
43
+ const peer = new FakePeer();
44
+ const client = new CatuiAcpClient(peer, "/vault");
45
+ await client.newSession();
46
+ await client.prompt("Hello");
47
+
48
+ assert.deepEqual(peer.calls.at(-1), {
49
+ method: "session/prompt",
50
+ params: { sessionId: "s1", prompt: [{ type: "text", text: "Hello" }] },
51
+ });
52
+ });
53
+
54
+ test("CatuiAcpClient cancels by notification", async () => {
55
+ const peer = new FakePeer();
56
+ const client = new CatuiAcpClient(peer, "/vault");
57
+ await client.newSession();
58
+ client.cancel();
59
+
60
+ assert.deepEqual(peer.calls.at(-1), { method: "session/cancel", params: { sessionId: "s1" } });
61
+ });
@@ -0,0 +1,74 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { EventEmitter } from "node:events";
4
+ import { NdjsonRpcPeer } from "../src/acp/ndjson-rpc";
5
+
6
+ class FakeReadable extends EventEmitter {
7
+ setEncoding(_encoding: BufferEncoding): void {}
8
+ }
9
+
10
+ class FakeWritable {
11
+ chunks: string[] = [];
12
+ write(chunk: string): boolean {
13
+ this.chunks.push(chunk);
14
+ return true;
15
+ }
16
+ }
17
+
18
+ test("NdjsonRpcPeer sends requests with incrementing ids", async () => {
19
+ const input = new FakeReadable();
20
+ const output = new FakeWritable();
21
+ const peer = new NdjsonRpcPeer(input as never, output as never);
22
+
23
+ const promise = peer.request("initialize", { protocolVersion: 1 });
24
+ input.emit("data", '{"jsonrpc":"2.0","id":1,"result":{"ok":true}}\n');
25
+
26
+ assert.deepEqual(await promise, { ok: true });
27
+ assert.equal(JSON.parse(output.chunks[0]!).method, "initialize");
28
+ assert.equal(JSON.parse(output.chunks[0]!).id, 1);
29
+ });
30
+
31
+ test("NdjsonRpcPeer dispatches agent requests and writes responses", async () => {
32
+ const input = new FakeReadable();
33
+ const output = new FakeWritable();
34
+ const peer = new NdjsonRpcPeer(input as never, output as never);
35
+
36
+ peer.onRequest("session/request_permission", async () => ({
37
+ outcome: { outcome: "selected", optionId: "allow_once" },
38
+ }));
39
+
40
+ input.emit("data", '{"jsonrpc":"2.0","id":9,"method":"session/request_permission","params":{"sessionId":"s"}}\n');
41
+
42
+ await new Promise((resolve) => setTimeout(resolve, 0));
43
+ const response = JSON.parse(output.chunks[0]!);
44
+ assert.equal(response.id, 9);
45
+ assert.equal(response.result.outcome.optionId, "allow_once");
46
+ });
47
+
48
+ test("NdjsonRpcPeer dispatches notifications", () => {
49
+ const input = new FakeReadable();
50
+ const output = new FakeWritable();
51
+ const peer = new NdjsonRpcPeer(input as never, output as never);
52
+ const seen: unknown[] = [];
53
+
54
+ peer.onNotification("session/update", (params) => seen.push(params));
55
+ input.emit("data", '{"jsonrpc":"2.0","method":"session/update","params":{"sessionId":"s","update":{"sessionUpdate":"agent_message_chunk"}}}\n');
56
+
57
+ assert.deepEqual(seen, [{ sessionId: "s", update: { sessionUpdate: "agent_message_chunk" } }]);
58
+ });
59
+
60
+ test("NdjsonRpcPeer ignores non-json startup output without rejecting pending requests", async () => {
61
+ const input = new FakeReadable();
62
+ const output = new FakeWritable();
63
+ const peer = new NdjsonRpcPeer(input as never, output as never);
64
+ const errors: string[] = [];
65
+ peer.onError((error) => errors.push(error.message));
66
+
67
+ const promise = peer.request("initialize", {});
68
+ input.emit("data", "MCP: 3 server(s) ready\n");
69
+ input.emit("data", '{"jsonrpc":"2.0","id":1,"result":{"ok":true}}\n');
70
+
71
+ assert.deepEqual(await promise, { ok: true });
72
+ assert.equal(errors.length, 1);
73
+ assert.match(errors[0]!, /Ignored non-ACP output/);
74
+ });
@@ -0,0 +1,31 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { classifyPermissionRisk, createPermissionOutcome } from "../src/policy/permission-policy";
4
+
5
+ test("classifyPermissionRisk blocks raw writes", () => {
6
+ const risk = classifyPermissionRisk({
7
+ protectRawWrites: true,
8
+ rawInput: { file_path: "/vault/raw/设计/source.md" },
9
+ });
10
+
11
+ assert.equal(risk.level, "blocked");
12
+ assert.match(risk.reason, /raw/);
13
+ });
14
+
15
+ test("classifyPermissionRisk allows wiki edits to be reviewed", () => {
16
+ const risk = classifyPermissionRisk({
17
+ protectRawWrites: true,
18
+ rawInput: { file_path: "/vault/wiki/design/AI应用.md" },
19
+ });
20
+
21
+ assert.equal(risk.level, "review");
22
+ });
23
+
24
+ test("createPermissionOutcome maps selected option ids", () => {
25
+ assert.deepEqual(createPermissionOutcome("allow_once"), {
26
+ outcome: { outcome: "selected", optionId: "allow_once" },
27
+ });
28
+ assert.deepEqual(createPermissionOutcome("cancelled"), {
29
+ outcome: { outcome: "cancelled" },
30
+ });
31
+ });