pi-tau-web-server 1.0.8
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 +303 -0
- package/bin/auth.js +96 -0
- package/bin/config.js +99 -0
- package/bin/model-utils.js +134 -0
- package/bin/server-main.js +1189 -0
- package/bin/sessions.js +492 -0
- package/bin/tau.js +8 -0
- package/bin/tree.js +248 -0
- package/bin/types.js +2 -0
- package/package.json +74 -0
- package/public/app-main.js +2025 -0
- package/public/app-types.js +1 -0
- package/public/app.js +1 -0
- package/public/command-palette.js +42 -0
- package/public/dialogs.js +199 -0
- package/public/file-browser.js +196 -0
- package/public/icons/apple-touch-icon.png +0 -0
- package/public/icons/favicon-16.png +0 -0
- package/public/icons/favicon-32.png +0 -0
- package/public/icons/tau-192.png +0 -0
- package/public/icons/tau-512.png +0 -0
- package/public/icons/tau-logo.png +0 -0
- package/public/icons/tau-logo.svg +13 -0
- package/public/icons/tau-maskable-512.png +0 -0
- package/public/icons/tau-new.png +0 -0
- package/public/index.html +264 -0
- package/public/launcher-panel.js +54 -0
- package/public/launcher.js +84 -0
- package/public/manifest.json +28 -0
- package/public/markdown.js +336 -0
- package/public/message-renderer.js +268 -0
- package/public/model-picker.js +478 -0
- package/public/session-sidebar.js +460 -0
- package/public/session-stats-card.js +123 -0
- package/public/state.js +81 -0
- package/public/style.css +3864 -0
- package/public/sw.js +66 -0
- package/public/themes.js +72 -0
- package/public/tool-card.js +317 -0
- package/public/tree-view.js +474 -0
- package/public/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/public/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/public/vendor/katex/katex.min.css +1 -0
- package/public/vendor/katex/katex.min.js +1 -0
- package/public/voice-input.js +74 -0
- package/public/websocket-client.js +156 -0
- package/scripts/copy-katex.mjs +32 -0
- package/src/pi-extension/tau-tree.ts +71 -0
- package/src/public/app-main.ts +2190 -0
- package/src/public/app-types.ts +96 -0
- package/src/public/app.ts +1 -0
- package/src/public/command-palette.ts +53 -0
- package/src/public/dialogs.ts +251 -0
- package/src/public/file-browser.ts +224 -0
- package/src/public/launcher-panel.ts +68 -0
- package/src/public/launcher.ts +101 -0
- package/src/public/legacy-dom.d.ts +29 -0
- package/src/public/markdown.ts +372 -0
- package/src/public/message-renderer.ts +311 -0
- package/src/public/model-picker.ts +500 -0
- package/src/public/session-sidebar.ts +522 -0
- package/src/public/session-stats-card.ts +176 -0
- package/src/public/state.ts +96 -0
- package/src/public/sw.ts +79 -0
- package/src/public/themes.ts +73 -0
- package/src/public/tool-card.ts +375 -0
- package/src/public/tree-view.ts +527 -0
- package/src/public/voice-input.ts +98 -0
- package/src/public/websocket-client.ts +165 -0
- package/src/server/auth.ts +88 -0
- package/src/server/config.ts +88 -0
- package/src/server/model-utils.ts +122 -0
- package/src/server/server-main.ts +1004 -0
- package/src/server/sessions.ts +481 -0
- package/src/server/tau.ts +9 -0
- package/src/server/tree.ts +288 -0
- package/src/server/types.ts +68 -0
- package/tsconfig.json +3 -0
- package/tsconfig.public.json +15 -0
- package/tsconfig.server.json +16 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type ModelRecord = {
|
|
2
|
+
provider?: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
context?: string | number;
|
|
8
|
+
contextWindow?: string | number;
|
|
9
|
+
context_window?: string | number;
|
|
10
|
+
maxOutput?: string | number;
|
|
11
|
+
max_output?: string | number;
|
|
12
|
+
maxOut?: string | number;
|
|
13
|
+
thinking?: boolean | string;
|
|
14
|
+
images?: boolean | string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type LiveSession = {
|
|
19
|
+
id: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
sessionFile?: string | null;
|
|
22
|
+
sessionName?: string | null;
|
|
23
|
+
modelSpec?: string;
|
|
24
|
+
modelLabel?: string;
|
|
25
|
+
model?: ModelRecord | string | null;
|
|
26
|
+
thinkingLevel?: string;
|
|
27
|
+
isStreaming?: boolean;
|
|
28
|
+
createdAt?: string;
|
|
29
|
+
lastActiveAt?: string;
|
|
30
|
+
contextUsage?: UsageRecord;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type LiveInstance = {
|
|
34
|
+
sessionFile?: string | null;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
port: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type UsageRecord = {
|
|
40
|
+
input?: number;
|
|
41
|
+
output?: number;
|
|
42
|
+
cacheRead?: number;
|
|
43
|
+
cacheWrite?: number;
|
|
44
|
+
cost?: { total?: number };
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MessageContentBlock = {
|
|
49
|
+
type?: string;
|
|
50
|
+
text?: string;
|
|
51
|
+
thinking?: string;
|
|
52
|
+
source?: { data?: string; media_type?: string };
|
|
53
|
+
data?: string;
|
|
54
|
+
media_type?: string;
|
|
55
|
+
id?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
arguments?: Record<string, unknown>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type AppMessage = {
|
|
61
|
+
id?: string;
|
|
62
|
+
role?: string;
|
|
63
|
+
content?: string | MessageContentBlock[];
|
|
64
|
+
usage?: UsageRecord;
|
|
65
|
+
images?: PendingImage[];
|
|
66
|
+
toolCallId?: string;
|
|
67
|
+
isError?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type AppEvent = {
|
|
71
|
+
type?: string;
|
|
72
|
+
sessionId?: string;
|
|
73
|
+
session?: LiveSession;
|
|
74
|
+
message?: AppMessage | string;
|
|
75
|
+
assistantMessageEvent?: { type?: string; delta?: string };
|
|
76
|
+
toolCallId?: string;
|
|
77
|
+
toolName?: string;
|
|
78
|
+
args?: Record<string, unknown>;
|
|
79
|
+
partialResult?: unknown;
|
|
80
|
+
result?: unknown;
|
|
81
|
+
isError?: boolean;
|
|
82
|
+
method?: string;
|
|
83
|
+
id?: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
error?: string;
|
|
86
|
+
summary?: string;
|
|
87
|
+
contextUsage?: UsageRecord;
|
|
88
|
+
sessionFile?: string;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type PendingImage = { data: string; mimeType: string };
|
|
93
|
+
export type PendingFilePath = { path: string; name: string; ext: string; sessionId?: string | null };
|
|
94
|
+
export type QueuedCommand = { type: string; message?: string; images?: PendingImage[]; sessionId?: string };
|
|
95
|
+
export type ExtensionUIRequest = { sessionId: string; event: AppEvent };
|
|
96
|
+
export type RpcCommand = { type: string; sessionId?: string; filePath?: string; [key: string]: unknown };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './app-main.js';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
type CommandPaletteItem = {
|
|
2
|
+
icon: string;
|
|
3
|
+
label: string;
|
|
4
|
+
desc: string;
|
|
5
|
+
action(): void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function setupCommandPalette(commands: CommandPaletteItem[]) {
|
|
9
|
+
// These elements are part of the app's static index.html shell, so they
|
|
10
|
+
// are present when this setup function runs; assert non-null at the query.
|
|
11
|
+
const commandBtn = document.getElementById('command-btn')!;
|
|
12
|
+
const commandPalette = document.getElementById('command-palette')!;
|
|
13
|
+
const commandPaletteOverlay = document.getElementById('command-palette-overlay')!;
|
|
14
|
+
const commandList = document.getElementById('command-list')!;
|
|
15
|
+
|
|
16
|
+
function open() {
|
|
17
|
+
commandList.innerHTML = '';
|
|
18
|
+
commands.forEach(cmd => {
|
|
19
|
+
const el = document.createElement('div');
|
|
20
|
+
el.className = 'command-item';
|
|
21
|
+
el.innerHTML = `
|
|
22
|
+
<div class="command-icon">${cmd.icon}</div>
|
|
23
|
+
<div>
|
|
24
|
+
<div class="command-label">${cmd.label}</div>
|
|
25
|
+
<div class="command-desc">${cmd.desc}</div>
|
|
26
|
+
</div>
|
|
27
|
+
`;
|
|
28
|
+
el.addEventListener('click', () => {
|
|
29
|
+
close();
|
|
30
|
+
cmd.action();
|
|
31
|
+
});
|
|
32
|
+
commandList.appendChild(el);
|
|
33
|
+
});
|
|
34
|
+
commandPalette.classList.remove('hidden');
|
|
35
|
+
commandPaletteOverlay.classList.remove('hidden');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function close() {
|
|
39
|
+
commandPalette.classList.add('hidden');
|
|
40
|
+
commandPaletteOverlay.classList.add('hidden');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function closeIfOpen() {
|
|
44
|
+
if (commandPalette.classList.contains('hidden')) return false;
|
|
45
|
+
close();
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
commandBtn.addEventListener('click', open);
|
|
50
|
+
commandPaletteOverlay.addEventListener('click', close);
|
|
51
|
+
|
|
52
|
+
return { close, closeIfOpen };
|
|
53
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialogs - Handles extension UI dialogs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type DialogRequest = {
|
|
6
|
+
id?: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
options?: string[];
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
prefill?: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class DialogHandler {
|
|
19
|
+
container: HTMLElement;
|
|
20
|
+
wsClient: { send(data: unknown): void };
|
|
21
|
+
getSessionId: (() => string | null) | null;
|
|
22
|
+
currentDialog: HTMLElement | null;
|
|
23
|
+
currentRequest: ({ sessionId?: string | null; request?: DialogRequest | null } & Record<string, unknown>) | null;
|
|
24
|
+
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
25
|
+
onIdle: (() => void) | null;
|
|
26
|
+
|
|
27
|
+
constructor(container: HTMLElement, wsClient: { send(data: unknown): void }, getSessionId: (() => string | null) | null = null) {
|
|
28
|
+
this.container = container;
|
|
29
|
+
this.wsClient = wsClient;
|
|
30
|
+
this.getSessionId = getSessionId;
|
|
31
|
+
this.currentDialog = null;
|
|
32
|
+
this.currentRequest = null;
|
|
33
|
+
this.timeoutId = null;
|
|
34
|
+
this.onIdle = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
showSelect(request: DialogRequest) {
|
|
38
|
+
this.cancelCurrentDialog(true);
|
|
39
|
+
|
|
40
|
+
const { id, title, options, timeout, sessionId } = request;
|
|
41
|
+
|
|
42
|
+
const dialog = document.createElement('div');
|
|
43
|
+
dialog.className = 'dialog';
|
|
44
|
+
dialog.innerHTML = `
|
|
45
|
+
<div class="dialog-title">${this.escapeHtml(title || 'Select an option')}</div>
|
|
46
|
+
<div class="dialog-options" id="dialog-options"></div>
|
|
47
|
+
<div class="dialog-actions">
|
|
48
|
+
<button id="dialog-cancel">Cancel</button>
|
|
49
|
+
</div>
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const optionsContainer = dialog.querySelector('#dialog-options')!;
|
|
53
|
+
|
|
54
|
+
(options || []).forEach((option: string) => {
|
|
55
|
+
const optionDiv = document.createElement('div');
|
|
56
|
+
optionDiv.className = 'dialog-option';
|
|
57
|
+
optionDiv.textContent = option;
|
|
58
|
+
optionDiv.onclick = () => {
|
|
59
|
+
this.respond(id, { value: option }, sessionId);
|
|
60
|
+
};
|
|
61
|
+
optionsContainer.appendChild(optionDiv);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
dialog.querySelector('#dialog-cancel')!.onclick = () => {
|
|
65
|
+
this.respond(id, { cancelled: true }, sessionId);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this.showDialog(dialog, timeout, id, sessionId, request);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
showConfirm(request: DialogRequest) {
|
|
72
|
+
this.cancelCurrentDialog(true);
|
|
73
|
+
|
|
74
|
+
const { id, title, message, timeout, sessionId } = request;
|
|
75
|
+
|
|
76
|
+
const dialog = document.createElement('div');
|
|
77
|
+
dialog.className = 'dialog';
|
|
78
|
+
dialog.innerHTML = `
|
|
79
|
+
<div class="dialog-title">${this.escapeHtml(title || 'Confirm')}</div>
|
|
80
|
+
${message ? `<div class="dialog-message">${this.escapeHtml(message)}</div>` : ''}
|
|
81
|
+
<div class="dialog-actions">
|
|
82
|
+
<button id="dialog-no">No</button>
|
|
83
|
+
<button id="dialog-yes">Yes</button>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
dialog.querySelector('#dialog-yes')!.onclick = () => {
|
|
88
|
+
this.respond(id, { confirmed: true }, sessionId);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
dialog.querySelector('#dialog-no')!.onclick = () => {
|
|
92
|
+
this.respond(id, { confirmed: false }, sessionId);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
this.showDialog(dialog, timeout, id, sessionId, request);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
showInput(request: DialogRequest) {
|
|
99
|
+
this.cancelCurrentDialog(true);
|
|
100
|
+
|
|
101
|
+
const { id, title, placeholder, timeout, sessionId } = request;
|
|
102
|
+
|
|
103
|
+
const dialog = document.createElement('div');
|
|
104
|
+
dialog.className = 'dialog';
|
|
105
|
+
dialog.innerHTML = `
|
|
106
|
+
<div class="dialog-title">${this.escapeHtml(title || 'Input')}</div>
|
|
107
|
+
<input type="text" class="dialog-input" id="dialog-input" placeholder="${this.escapeHtml(placeholder || '')}" />
|
|
108
|
+
<div class="dialog-actions">
|
|
109
|
+
<button id="dialog-cancel">Cancel</button>
|
|
110
|
+
<button id="dialog-submit">Submit</button>
|
|
111
|
+
</div>
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
const input = dialog.querySelector<HTMLInputElement>('#dialog-input');
|
|
115
|
+
if (!input) return;
|
|
116
|
+
|
|
117
|
+
const submit = () => {
|
|
118
|
+
const value = input.value.trim();
|
|
119
|
+
this.respond(id, value ? { value } : { cancelled: true }, sessionId);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
input.addEventListener('keypress', (e: KeyboardEvent) => {
|
|
123
|
+
if (e.key === 'Enter') submit();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
dialog.querySelector('#dialog-submit')!.onclick = submit;
|
|
127
|
+
dialog.querySelector('#dialog-cancel')!.onclick = () => {
|
|
128
|
+
this.respond(id, { cancelled: true }, sessionId);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
this.showDialog(dialog, timeout, id, sessionId, request);
|
|
132
|
+
|
|
133
|
+
// Focus input after a short delay
|
|
134
|
+
setTimeout(() => input.focus(), 100);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
showEditor(request: DialogRequest) {
|
|
138
|
+
this.cancelCurrentDialog(true);
|
|
139
|
+
|
|
140
|
+
const { id, title, prefill, timeout, sessionId } = request;
|
|
141
|
+
|
|
142
|
+
const dialog = document.createElement('div');
|
|
143
|
+
dialog.className = 'dialog';
|
|
144
|
+
dialog.innerHTML = `
|
|
145
|
+
<div class="dialog-title">${this.escapeHtml(title || 'Editor')}</div>
|
|
146
|
+
<textarea class="dialog-textarea" id="dialog-textarea">${this.escapeHtml(prefill || '')}</textarea>
|
|
147
|
+
<div class="dialog-actions">
|
|
148
|
+
<button id="dialog-cancel">Cancel</button>
|
|
149
|
+
<button id="dialog-save">Save</button>
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
const textarea = dialog.querySelector<HTMLTextAreaElement>('#dialog-textarea');
|
|
154
|
+
if (!textarea) return;
|
|
155
|
+
|
|
156
|
+
dialog.querySelector('#dialog-save')!.onclick = () => {
|
|
157
|
+
const value = textarea.value;
|
|
158
|
+
this.respond(id, value ? { value } : { cancelled: true }, sessionId);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
dialog.querySelector('#dialog-cancel')!.onclick = () => {
|
|
162
|
+
this.respond(id, { cancelled: true }, sessionId);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
this.showDialog(dialog, timeout, id, sessionId, request);
|
|
166
|
+
|
|
167
|
+
// Focus textarea after a short delay
|
|
168
|
+
setTimeout(() => textarea.focus(), 100);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
showNotification(request: DialogRequest) {
|
|
172
|
+
const { message, notifyType } = request;
|
|
173
|
+
|
|
174
|
+
// Create a temporary notification element
|
|
175
|
+
const notification = document.createElement('div');
|
|
176
|
+
notification.className = 'error-message';
|
|
177
|
+
notification.textContent = `${notifyType === 'error' ? '⚠️' : notifyType === 'warning' ? '⚠️' : 'ℹ️'} ${message}`;
|
|
178
|
+
|
|
179
|
+
// Add to messages container temporarily
|
|
180
|
+
const messagesContainer = document.getElementById('messages');
|
|
181
|
+
if (messagesContainer) {
|
|
182
|
+
messagesContainer.appendChild(notification);
|
|
183
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
184
|
+
|
|
185
|
+
// Remove after 5 seconds
|
|
186
|
+
setTimeout(() => {
|
|
187
|
+
notification.remove();
|
|
188
|
+
}, 5000);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
showDialog(dialogElement: HTMLElement, timeout: number | undefined, requestId: string | undefined, sessionId: string | null = null, request: DialogRequest | null = null) {
|
|
193
|
+
this.currentDialog = dialogElement;
|
|
194
|
+
this.currentRequest = { id: requestId, sessionId, request };
|
|
195
|
+
this.container.innerHTML = '';
|
|
196
|
+
this.container.appendChild(dialogElement);
|
|
197
|
+
this.container.classList.remove('hidden');
|
|
198
|
+
|
|
199
|
+
// Set up timeout if specified
|
|
200
|
+
if (timeout) {
|
|
201
|
+
this.timeoutId = setTimeout(() => {
|
|
202
|
+
this.respond(requestId, { cancelled: true }, sessionId);
|
|
203
|
+
}, timeout);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
cancelCurrentDialog(suppressIdle = false) {
|
|
208
|
+
if (!this.currentRequest?.id) {
|
|
209
|
+
this.clearCurrentDialog();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const { id, sessionId } = this.currentRequest;
|
|
213
|
+
this.clearCurrentDialog();
|
|
214
|
+
this.wsClient.send({
|
|
215
|
+
type: 'extension_ui_response',
|
|
216
|
+
id,
|
|
217
|
+
sessionId: sessionId || this.getSessionId?.(),
|
|
218
|
+
cancelled: true,
|
|
219
|
+
});
|
|
220
|
+
if (!suppressIdle) this.onIdle?.();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
clearCurrentDialog() {
|
|
224
|
+
if (this.timeoutId) {
|
|
225
|
+
clearTimeout(this.timeoutId);
|
|
226
|
+
this.timeoutId = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
this.container.innerHTML = '';
|
|
230
|
+
this.container.classList.add('hidden');
|
|
231
|
+
this.currentDialog = null;
|
|
232
|
+
this.currentRequest = null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
respond(id: string | undefined, response: Record<string, unknown>, sessionId: string | null = null) {
|
|
236
|
+
this.clearCurrentDialog();
|
|
237
|
+
this.wsClient.send({
|
|
238
|
+
type: 'extension_ui_response',
|
|
239
|
+
id,
|
|
240
|
+
sessionId: sessionId || this.getSessionId?.(),
|
|
241
|
+
...response
|
|
242
|
+
});
|
|
243
|
+
this.onIdle?.();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
escapeHtml(text: string) {
|
|
247
|
+
const div = document.createElement('div');
|
|
248
|
+
div.textContent = text;
|
|
249
|
+
return div.innerHTML;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Browser — right sidebar file tree with drag-and-drop
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const FILE_ICONS: Record<string, string> = {
|
|
6
|
+
// Folders
|
|
7
|
+
directory: '📁',
|
|
8
|
+
// Code
|
|
9
|
+
js: '📄', ts: '📄', jsx: '📄', tsx: '📄',
|
|
10
|
+
py: '🐍', rb: '💎', go: '📄', rs: '🦀',
|
|
11
|
+
// Web
|
|
12
|
+
html: '🌐', css: '🎨', svg: '🎨',
|
|
13
|
+
// Data
|
|
14
|
+
json: '📋', yaml: '📋', yml: '📋', toml: '📋',
|
|
15
|
+
xml: '📋', csv: '📋',
|
|
16
|
+
// Docs
|
|
17
|
+
md: '📝', txt: '📝', rst: '📝',
|
|
18
|
+
// Images
|
|
19
|
+
png: '🖼️', jpg: '🖼️', jpeg: '🖼️', gif: '🖼️',
|
|
20
|
+
webp: '🖼️', ico: '🖼️',
|
|
21
|
+
// Config
|
|
22
|
+
env: '🔒', gitignore: '🔒', lock: '🔒',
|
|
23
|
+
// Default
|
|
24
|
+
default: '📄',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type FileItem = {
|
|
28
|
+
name: string;
|
|
29
|
+
path: string;
|
|
30
|
+
isDirectory: boolean;
|
|
31
|
+
size?: number | null;
|
|
32
|
+
mtime?: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function getFileIcon(name: string, isDirectory: boolean) {
|
|
36
|
+
if (isDirectory) return FILE_ICONS.directory;
|
|
37
|
+
const ext = name.split('.').pop()?.toLowerCase() || '';
|
|
38
|
+
return FILE_ICONS[ext] || FILE_ICONS.default;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatSize(bytes?: number | null) {
|
|
42
|
+
if (bytes == null) return '';
|
|
43
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
44
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}K`;
|
|
45
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class FileBrowser {
|
|
49
|
+
container: HTMLElement;
|
|
50
|
+
pathEl: HTMLElement;
|
|
51
|
+
messageInput: HTMLElement;
|
|
52
|
+
onFileInserted: ((filePath: string) => void) | null;
|
|
53
|
+
getSessionId: (() => string | null) | null;
|
|
54
|
+
currentPath: string | null;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
container: HTMLElement,
|
|
58
|
+
pathEl: HTMLElement,
|
|
59
|
+
messageInput: HTMLElement,
|
|
60
|
+
onFileInserted: ((filePath: string) => void) | null = null,
|
|
61
|
+
getSessionId: (() => string | null) | null = null
|
|
62
|
+
) {
|
|
63
|
+
this.container = container;
|
|
64
|
+
this.pathEl = pathEl;
|
|
65
|
+
this.messageInput = messageInput;
|
|
66
|
+
this.onFileInserted = onFileInserted;
|
|
67
|
+
this.getSessionId = getSessionId;
|
|
68
|
+
this.currentPath = null;
|
|
69
|
+
|
|
70
|
+
this.setupDropTarget();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async load(dirPath?: string | null) {
|
|
74
|
+
this.container.innerHTML = '<div class="file-loading">Loading…</div>';
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const params = new URLSearchParams();
|
|
78
|
+
const sessionId = this.getSessionId?.();
|
|
79
|
+
if (!sessionId) {
|
|
80
|
+
this.currentPath = null;
|
|
81
|
+
this.pathEl.textContent = '';
|
|
82
|
+
this.pathEl.title = '';
|
|
83
|
+
this.container.innerHTML = '<div class="file-loading">Select a Tau tab to browse files</div>';
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
params.set('sessionId', sessionId);
|
|
87
|
+
if (dirPath) params.set('path', dirPath);
|
|
88
|
+
const qs = params.toString();
|
|
89
|
+
const url = qs ? `/api/files?${qs}` : '/api/files';
|
|
90
|
+
const res = await fetch(url);
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
|
|
93
|
+
if (data.error) {
|
|
94
|
+
this.container.innerHTML = `<div class="file-loading">${data.error}</div>`;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.currentPath = data.path;
|
|
99
|
+
this.pathEl.textContent = data.path;
|
|
100
|
+
this.pathEl.title = data.path;
|
|
101
|
+
this.render(data.items);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
this.container.innerHTML = '<div class="file-loading">Failed to load</div>';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getParentPath() {
|
|
108
|
+
if (!this.currentPath) return null;
|
|
109
|
+
const sep = this.currentPath.includes('\\') ? '\\' : '/';
|
|
110
|
+
const normalized = this.currentPath.endsWith(sep) ? this.currentPath.slice(0, -1) : this.currentPath;
|
|
111
|
+
const lastSep = normalized.lastIndexOf(sep);
|
|
112
|
+
if (lastSep <= 0) return sep === '/' ? '/' : null;
|
|
113
|
+
const parent = normalized.slice(0, lastSep);
|
|
114
|
+
return /^[A-Za-z]:$/.test(parent) ? parent + sep : parent;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render(items: FileItem[]) {
|
|
118
|
+
this.container.innerHTML = '';
|
|
119
|
+
|
|
120
|
+
if (items.length === 0) {
|
|
121
|
+
this.container.innerHTML = '<div class="file-loading">Empty directory</div>';
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
const el = document.createElement('div');
|
|
127
|
+
el.className = `file-item${item.isDirectory ? ' directory' : ''}`;
|
|
128
|
+
el.draggable = true;
|
|
129
|
+
el.dataset.path = item.path;
|
|
130
|
+
el.dataset.name = item.name;
|
|
131
|
+
el.dataset.isDirectory = String(item.isDirectory);
|
|
132
|
+
|
|
133
|
+
const icon = getFileIcon(item.name, item.isDirectory);
|
|
134
|
+
const size = item.isDirectory ? '' : formatSize(item.size);
|
|
135
|
+
|
|
136
|
+
el.innerHTML = `
|
|
137
|
+
<span class="file-icon">${icon}</span>
|
|
138
|
+
<span class="file-name" title="${item.name}">${item.name}</span>
|
|
139
|
+
${size ? `<span class="file-size">${size}</span>` : ''}
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
// Click: navigate directory or insert file path into input
|
|
143
|
+
el.addEventListener('click', () => {
|
|
144
|
+
if (item.isDirectory) {
|
|
145
|
+
this.load(item.path);
|
|
146
|
+
} else {
|
|
147
|
+
this.insertPath(item.path);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Double-click: open file natively
|
|
152
|
+
el.addEventListener('dblclick', (e) => {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
if (!item.isDirectory) {
|
|
155
|
+
this.openNatively(item.path);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Drag start
|
|
160
|
+
el.addEventListener('dragstart', (e) => {
|
|
161
|
+
if (!e.dataTransfer) return;
|
|
162
|
+
e.dataTransfer.setData('text/plain', item.path);
|
|
163
|
+
e.dataTransfer.effectAllowed = 'copy';
|
|
164
|
+
el.classList.add('dragging');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
el.addEventListener('dragend', () => {
|
|
168
|
+
el.classList.remove('dragging');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
this.container.appendChild(el);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async openNatively(filePath: string) {
|
|
176
|
+
try {
|
|
177
|
+
const sessionId = this.getSessionId?.();
|
|
178
|
+
if (!sessionId) return;
|
|
179
|
+
await fetch('/api/open', {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify({ filePath, sessionId }),
|
|
183
|
+
});
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error('[FileBrowser] Failed to open:', err);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
insertPath(filePath: string) {
|
|
190
|
+
const input = this.messageInput;
|
|
191
|
+
const start = input.selectionStart ?? 0;
|
|
192
|
+
const end = input.selectionEnd ?? 0;
|
|
193
|
+
input.value = input.value.substring(0, start) + filePath + ' ' + input.value.substring(end);
|
|
194
|
+
input.selectionStart = input.selectionEnd = start + filePath.length + 1;
|
|
195
|
+
input.focus();
|
|
196
|
+
input.dispatchEvent(new Event('input'));
|
|
197
|
+
if (this.onFileInserted) this.onFileInserted(filePath);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setupDropTarget() {
|
|
201
|
+
const input = this.messageInput;
|
|
202
|
+
|
|
203
|
+
input.addEventListener('dragover', (e) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
206
|
+
input.classList.add('file-drop-hover');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
input.addEventListener('dragleave', () => {
|
|
210
|
+
input.classList.remove('file-drop-hover');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
input.addEventListener('drop', (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
input.classList.remove('file-drop-hover');
|
|
216
|
+
|
|
217
|
+
const filePath = e.dataTransfer?.getData('text/plain') ?? '';
|
|
218
|
+
// Accept Unix paths (/) and Windows paths (C:\ or C:/)
|
|
219
|
+
if (filePath && (filePath.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(filePath))) {
|
|
220
|
+
this.insertPath(filePath);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Launcher } from './launcher.js';
|
|
2
|
+
|
|
3
|
+
type LauncherPanelOptions = {
|
|
4
|
+
launcherEl: HTMLElement;
|
|
5
|
+
messagesContainer: HTMLElement;
|
|
6
|
+
createSession(projectPath: string): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function setupLauncherPanel({ launcherEl, messagesContainer, createSession }: LauncherPanelOptions) {
|
|
10
|
+
const launcher = new Launcher(launcherEl, createSession);
|
|
11
|
+
|
|
12
|
+
async function init() {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch('/api/projects');
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
if (data.projects && data.projects.length > 0) {
|
|
17
|
+
launcher.projects = data.projects;
|
|
18
|
+
launcher.render();
|
|
19
|
+
addLauncherNav();
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function addLauncherNav() {
|
|
25
|
+
const modeToggle = document.getElementById('mode-toggle');
|
|
26
|
+
if (!modeToggle || modeToggle.querySelector('.mode-link-launcher')) return;
|
|
27
|
+
|
|
28
|
+
const launcherLink = document.createElement('span');
|
|
29
|
+
launcherLink.className = 'mode-link mode-link-launcher';
|
|
30
|
+
launcherLink.title = 'Projects';
|
|
31
|
+
launcherLink.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>';
|
|
32
|
+
launcherLink.addEventListener('click', () => show());
|
|
33
|
+
modeToggle.appendChild(launcherLink);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isVisible() {
|
|
37
|
+
return !!launcherEl && !launcherEl.classList.contains('hidden');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function show() {
|
|
41
|
+
launcherEl.classList.remove('hidden');
|
|
42
|
+
messagesContainer.style.display = 'none';
|
|
43
|
+
const inputArea = document.querySelector<HTMLElement>('.input-area');
|
|
44
|
+
if (inputArea) inputArea.style.display = 'none';
|
|
45
|
+
document.querySelector('.welcome')?.remove();
|
|
46
|
+
|
|
47
|
+
document.querySelectorAll('.mode-link').forEach(l => l.classList.remove('active'));
|
|
48
|
+
document.querySelector('.mode-link-launcher')?.classList.add('active');
|
|
49
|
+
|
|
50
|
+
launcher.load();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function hide() {
|
|
54
|
+
launcherEl.classList.add('hidden');
|
|
55
|
+
messagesContainer.style.display = '';
|
|
56
|
+
const inputArea = document.querySelector<HTMLElement>('.input-area');
|
|
57
|
+
if (inputArea) inputArea.style.display = '';
|
|
58
|
+
|
|
59
|
+
document.querySelectorAll('.mode-link').forEach(l => l.classList.remove('active'));
|
|
60
|
+
document.querySelector('.mode-link:first-child')?.classList.add('active');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
document.querySelector('.mode-link:first-child')?.addEventListener('click', () => {
|
|
64
|
+
hide();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { init, isVisible, show, hide };
|
|
68
|
+
}
|