@agent-link/server 0.1.55 → 0.1.57
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/dist/auth.d.ts +13 -0
- package/dist/auth.js +65 -0
- package/dist/auth.js.map +1 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +5 -0
- package/dist/context.js.map +1 -1
- package/dist/ws-agent.js +13 -1
- package/dist/ws-agent.js.map +1 -1
- package/dist/ws-client.js +104 -5
- package/dist/ws-client.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +73 -26
- package/web/modules/connection.js +52 -2
- package/web/modules/sidebar.js +10 -0
- package/web/style.css +169 -73
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function hashPassword(password: string): {
|
|
2
|
+
hash: string;
|
|
3
|
+
salt: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function verifyPassword(submitted: string, storedHash: string, storedSalt: string): boolean;
|
|
6
|
+
export declare function generateAuthToken(sessionId: string): string;
|
|
7
|
+
export declare function verifyAuthToken(token: string, expectedSessionId: string): boolean;
|
|
8
|
+
export declare function isSessionLocked(sessionId: string): boolean;
|
|
9
|
+
export declare function recordFailure(sessionId: string): {
|
|
10
|
+
locked: boolean;
|
|
11
|
+
remaining: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function clearFailures(sessionId: string): void;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { scryptSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
2
|
+
import { authAttempts, serverSecret } from './context.js';
|
|
3
|
+
const MAX_FAILURES = 5;
|
|
4
|
+
const LOCKOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
5
|
+
const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
export function hashPassword(password) {
|
|
7
|
+
const salt = randomBytes(16).toString('base64');
|
|
8
|
+
const hash = scryptSync(password, salt, 32).toString('base64');
|
|
9
|
+
return { hash, salt };
|
|
10
|
+
}
|
|
11
|
+
export function verifyPassword(submitted, storedHash, storedSalt) {
|
|
12
|
+
const computed = scryptSync(submitted, storedSalt, 32);
|
|
13
|
+
const expected = Buffer.from(storedHash, 'base64');
|
|
14
|
+
if (computed.length !== expected.length)
|
|
15
|
+
return false;
|
|
16
|
+
return timingSafeEqual(computed, expected);
|
|
17
|
+
}
|
|
18
|
+
export function generateAuthToken(sessionId) {
|
|
19
|
+
const ts = Date.now().toString();
|
|
20
|
+
const hmac = createHmac('sha256', serverSecret).update(`${sessionId}:${ts}`).digest('base64url');
|
|
21
|
+
return `${sessionId}:${ts}:${hmac}`;
|
|
22
|
+
}
|
|
23
|
+
export function verifyAuthToken(token, expectedSessionId) {
|
|
24
|
+
const idx1 = token.indexOf(':');
|
|
25
|
+
const idx2 = token.indexOf(':', idx1 + 1);
|
|
26
|
+
if (idx1 === -1 || idx2 === -1)
|
|
27
|
+
return false;
|
|
28
|
+
const sessionId = token.slice(0, idx1);
|
|
29
|
+
const ts = token.slice(idx1 + 1, idx2);
|
|
30
|
+
const hmac = token.slice(idx2 + 1);
|
|
31
|
+
if (sessionId !== expectedSessionId)
|
|
32
|
+
return false;
|
|
33
|
+
const age = Date.now() - parseInt(ts, 10);
|
|
34
|
+
if (isNaN(age) || age < 0 || age > TOKEN_TTL_MS)
|
|
35
|
+
return false;
|
|
36
|
+
const expected = createHmac('sha256', serverSecret).update(`${sessionId}:${ts}`).digest('base64url');
|
|
37
|
+
return hmac === expected;
|
|
38
|
+
}
|
|
39
|
+
export function isSessionLocked(sessionId) {
|
|
40
|
+
const state = authAttempts.get(sessionId);
|
|
41
|
+
if (!state?.lockedUntil)
|
|
42
|
+
return false;
|
|
43
|
+
if (Date.now() > state.lockedUntil.getTime()) {
|
|
44
|
+
authAttempts.delete(sessionId);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
export function recordFailure(sessionId) {
|
|
50
|
+
let state = authAttempts.get(sessionId);
|
|
51
|
+
if (!state) {
|
|
52
|
+
state = { failures: 0, lockedUntil: null };
|
|
53
|
+
authAttempts.set(sessionId, state);
|
|
54
|
+
}
|
|
55
|
+
state.failures++;
|
|
56
|
+
if (state.failures >= MAX_FAILURES) {
|
|
57
|
+
state.lockedUntil = new Date(Date.now() + LOCKOUT_MS);
|
|
58
|
+
return { locked: true, remaining: 0 };
|
|
59
|
+
}
|
|
60
|
+
return { locked: false, remaining: MAX_FAILURES - state.failures };
|
|
61
|
+
}
|
|
62
|
+
export function clearFailures(sessionId) {
|
|
63
|
+
authAttempts.delete(sessionId);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/context.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface AgentSession {
|
|
|
10
10
|
sessionKey: Uint8Array | null;
|
|
11
11
|
connectedAt: Date;
|
|
12
12
|
isAlive: boolean;
|
|
13
|
+
passwordHash: string | null;
|
|
14
|
+
passwordSalt: string | null;
|
|
13
15
|
}
|
|
14
16
|
export interface WebClient {
|
|
15
17
|
ws: WebSocket;
|
|
@@ -22,6 +24,13 @@ export interface WebClient {
|
|
|
22
24
|
export declare const agents: Map<string, AgentSession>;
|
|
23
25
|
export declare const sessionToAgent: Map<string, string>;
|
|
24
26
|
export declare const webClients: Map<string, WebClient>;
|
|
27
|
+
export interface AuthAttemptState {
|
|
28
|
+
failures: number;
|
|
29
|
+
lockedUntil: Date | null;
|
|
30
|
+
}
|
|
31
|
+
export declare const authAttempts: Map<string, AuthAttemptState>;
|
|
32
|
+
export declare const pendingAuth: Map<string, string>;
|
|
33
|
+
export declare const serverSecret: NonSharedBuffer;
|
|
25
34
|
/**
|
|
26
35
|
* Generate a short, URL-safe session ID
|
|
27
36
|
*/
|
package/dist/context.js
CHANGED
|
@@ -5,6 +5,11 @@ export const agents = new Map();
|
|
|
5
5
|
export const sessionToAgent = new Map();
|
|
6
6
|
// Web clients: clientId → WebClient
|
|
7
7
|
export const webClients = new Map();
|
|
8
|
+
export const authAttempts = new Map();
|
|
9
|
+
// Pending auth: clientId → sessionId (web clients awaiting password verification)
|
|
10
|
+
export const pendingAuth = new Map();
|
|
11
|
+
// Server secret for HMAC auth tokens (generated fresh on each server start)
|
|
12
|
+
export const serverSecret = randomBytes(32);
|
|
8
13
|
/**
|
|
9
14
|
* Generate a short, URL-safe session ID
|
|
10
15
|
*/
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AA0BrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAOvD,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,GAAG,EAA4B,CAAC;AAEhE,kFAAkF;AAClF,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;AAErD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;AAE5C;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
|
package/dist/ws-agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import { WebSocket } from 'ws';
|
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import { agents, sessionToAgent, webClients, generateSessionId, } from './context.js';
|
|
4
4
|
import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
|
|
5
|
+
import { hashPassword } from './auth.js';
|
|
5
6
|
export function handleAgentConnection(ws, req) {
|
|
6
7
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
7
8
|
const agentId = url.searchParams.get('id') || randomUUID();
|
|
@@ -9,6 +10,15 @@ export function handleAgentConnection(ws, req) {
|
|
|
9
10
|
const workDir = url.searchParams.get('workDir') || 'unknown';
|
|
10
11
|
const hostname = url.searchParams.get('hostname') || '';
|
|
11
12
|
const version = url.searchParams.get('version') || '';
|
|
13
|
+
const password = url.searchParams.get('password') || '';
|
|
14
|
+
// Hash password if provided (agent sends plaintext over WSS)
|
|
15
|
+
let passwordHash = null;
|
|
16
|
+
let passwordSalt = null;
|
|
17
|
+
if (password) {
|
|
18
|
+
const h = hashPassword(password);
|
|
19
|
+
passwordHash = h.hash;
|
|
20
|
+
passwordSalt = h.salt;
|
|
21
|
+
}
|
|
12
22
|
// Reuse requested sessionId (agent reconnecting) or generate a new one
|
|
13
23
|
const requestedSessionId = url.searchParams.get('sessionId');
|
|
14
24
|
const sessionId = requestedSessionId || generateSessionId();
|
|
@@ -24,10 +34,12 @@ export function handleAgentConnection(ws, req) {
|
|
|
24
34
|
sessionKey,
|
|
25
35
|
connectedAt: new Date(),
|
|
26
36
|
isAlive: true,
|
|
37
|
+
passwordHash,
|
|
38
|
+
passwordSalt,
|
|
27
39
|
};
|
|
28
40
|
agents.set(agentId, agent);
|
|
29
41
|
sessionToAgent.set(sessionId, agentId);
|
|
30
|
-
console.log(`[Agent] Registered: ${name} (${agentId}), session: ${sessionId}${requestedSessionId ? ' (reconnect)' : ''}`);
|
|
42
|
+
console.log(`[Agent] Registered: ${name} (${agentId}), session: ${sessionId}${requestedSessionId ? ' (reconnect)' : ''}${passwordHash ? ' (password protected)' : ''}`);
|
|
31
43
|
// Send registration with session key (this initial message is plain text)
|
|
32
44
|
ws.send(JSON.stringify({
|
|
33
45
|
type: 'registered',
|
package/dist/ws-agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EACL,MAAM,EACN,cAAc,EACd,UAAU,EACV,iBAAiB,GAElB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/ws-client.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { WebSocket } from 'ws';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
-
import { agents, sessionToAgent, webClients, } from './context.js';
|
|
4
|
+
import { agents, sessionToAgent, webClients, pendingAuth, } from './context.js';
|
|
5
5
|
import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
|
|
6
|
+
import { isSessionLocked, recordFailure, clearFailures, verifyPassword, generateAuthToken, verifyAuthToken, } from './auth.js';
|
|
6
7
|
const require = createRequire(import.meta.url);
|
|
7
8
|
const serverPkg = require('../package.json');
|
|
8
9
|
export function handleWebConnection(ws, req) {
|
|
9
10
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
10
11
|
const sessionId = url.searchParams.get('sessionId');
|
|
12
|
+
const authToken = url.searchParams.get('authToken');
|
|
11
13
|
const clientId = randomUUID();
|
|
12
14
|
if (!sessionId) {
|
|
13
15
|
ws.send(JSON.stringify({ type: 'error', message: 'Missing sessionId' }));
|
|
@@ -17,6 +19,99 @@ export function handleWebConnection(ws, req) {
|
|
|
17
19
|
// Check if agent exists for this session
|
|
18
20
|
const agentId = sessionToAgent.get(sessionId);
|
|
19
21
|
const agent = agentId ? agents.get(agentId) : undefined;
|
|
22
|
+
// Password-protected session?
|
|
23
|
+
const requiresAuth = !!(agent?.passwordHash && agent?.passwordSalt);
|
|
24
|
+
if (requiresAuth) {
|
|
25
|
+
// Check saved auth token first
|
|
26
|
+
if (authToken && verifyAuthToken(authToken, sessionId)) {
|
|
27
|
+
completeConnection(ws, clientId, sessionId, agent);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Check lockout
|
|
31
|
+
if (isSessionLocked(sessionId)) {
|
|
32
|
+
ws.send(JSON.stringify({
|
|
33
|
+
type: 'auth_locked',
|
|
34
|
+
message: 'Too many failed attempts. Try again in 15 minutes.',
|
|
35
|
+
}));
|
|
36
|
+
ws.close();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Require authentication
|
|
40
|
+
pendingAuth.set(clientId, sessionId);
|
|
41
|
+
ws.send(JSON.stringify({ type: 'auth_required', sessionId }));
|
|
42
|
+
ws.on('message', (data) => {
|
|
43
|
+
handlePendingAuthMessage(clientId, ws, data.toString());
|
|
44
|
+
});
|
|
45
|
+
ws.on('close', () => {
|
|
46
|
+
pendingAuth.delete(clientId);
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// No auth required — proceed directly
|
|
51
|
+
completeConnection(ws, clientId, sessionId, agent);
|
|
52
|
+
}
|
|
53
|
+
function handlePendingAuthMessage(clientId, ws, raw) {
|
|
54
|
+
const sessionId = pendingAuth.get(clientId);
|
|
55
|
+
if (!sessionId)
|
|
56
|
+
return;
|
|
57
|
+
let msg;
|
|
58
|
+
try {
|
|
59
|
+
msg = JSON.parse(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (msg.type !== 'authenticate' || typeof msg.password !== 'string')
|
|
65
|
+
return;
|
|
66
|
+
const agentId = sessionToAgent.get(sessionId);
|
|
67
|
+
const agent = agentId ? agents.get(agentId) : undefined;
|
|
68
|
+
if (!agent?.passwordHash || !agent?.passwordSalt) {
|
|
69
|
+
// Agent disconnected or password removed while authenticating
|
|
70
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Session no longer available.' }));
|
|
71
|
+
pendingAuth.delete(clientId);
|
|
72
|
+
ws.close();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Check lockout (may have been triggered by another client)
|
|
76
|
+
if (isSessionLocked(sessionId)) {
|
|
77
|
+
ws.send(JSON.stringify({
|
|
78
|
+
type: 'auth_locked',
|
|
79
|
+
message: 'Too many failed attempts. Try again in 15 minutes.',
|
|
80
|
+
}));
|
|
81
|
+
pendingAuth.delete(clientId);
|
|
82
|
+
ws.close();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const valid = verifyPassword(msg.password, agent.passwordHash, agent.passwordSalt);
|
|
86
|
+
if (!valid) {
|
|
87
|
+
const { locked, remaining } = recordFailure(sessionId);
|
|
88
|
+
if (locked) {
|
|
89
|
+
ws.send(JSON.stringify({
|
|
90
|
+
type: 'auth_locked',
|
|
91
|
+
message: 'Too many failed attempts. Try again in 15 minutes.',
|
|
92
|
+
}));
|
|
93
|
+
pendingAuth.delete(clientId);
|
|
94
|
+
ws.close();
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
ws.send(JSON.stringify({
|
|
98
|
+
type: 'auth_failed',
|
|
99
|
+
message: 'Incorrect password.',
|
|
100
|
+
attemptsRemaining: remaining,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Success
|
|
106
|
+
clearFailures(sessionId);
|
|
107
|
+
pendingAuth.delete(clientId);
|
|
108
|
+
const token = generateAuthToken(sessionId);
|
|
109
|
+
// Replace pending auth message handler with normal encrypted handler
|
|
110
|
+
ws.removeAllListeners('message');
|
|
111
|
+
ws.removeAllListeners('close');
|
|
112
|
+
completeConnection(ws, clientId, sessionId, agent, token);
|
|
113
|
+
}
|
|
114
|
+
function completeConnection(ws, clientId, sessionId, agent, authToken) {
|
|
20
115
|
const sessionKey = generateSessionKey();
|
|
21
116
|
const client = {
|
|
22
117
|
ws,
|
|
@@ -27,8 +122,8 @@ export function handleWebConnection(ws, req) {
|
|
|
27
122
|
isAlive: true,
|
|
28
123
|
};
|
|
29
124
|
webClients.set(clientId, client);
|
|
30
|
-
//
|
|
31
|
-
|
|
125
|
+
// Build connected payload
|
|
126
|
+
const payload = {
|
|
32
127
|
type: 'connected',
|
|
33
128
|
clientId,
|
|
34
129
|
sessionKey: encodeKey(sessionKey),
|
|
@@ -40,8 +135,12 @@ export function handleWebConnection(ws, req) {
|
|
|
40
135
|
workDir: agent.workDir,
|
|
41
136
|
version: agent.version,
|
|
42
137
|
} : null,
|
|
43
|
-
}
|
|
44
|
-
|
|
138
|
+
};
|
|
139
|
+
if (authToken) {
|
|
140
|
+
payload.authToken = authToken;
|
|
141
|
+
}
|
|
142
|
+
ws.send(JSON.stringify(payload));
|
|
143
|
+
console.log(`[Web] Client ${clientId.slice(0, 8)} connected to session ${sessionId}, agent: ${agent ? agent.name : 'none'}${authToken ? ' (authenticated)' : ''}`);
|
|
45
144
|
ws.on('message', (data) => {
|
|
46
145
|
handleWebMessage(clientId, data.toString());
|
|
47
146
|
});
|
package/dist/ws-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EACL,MAAM,EACN,cAAc,EACd,UAAU,
|
|
1
|
+
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../src/ws-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -72,6 +72,13 @@ const App = {
|
|
|
72
72
|
const workDirHistory = ref([]);
|
|
73
73
|
const workDirHistoryOpen = ref(false);
|
|
74
74
|
|
|
75
|
+
// Authentication state
|
|
76
|
+
const authRequired = ref(false);
|
|
77
|
+
const authPassword = ref('');
|
|
78
|
+
const authError = ref('');
|
|
79
|
+
const authAttempts = ref(null);
|
|
80
|
+
const authLocked = ref(false);
|
|
81
|
+
|
|
75
82
|
// File attachment state
|
|
76
83
|
const attachments = ref([]);
|
|
77
84
|
const fileInputRef = ref(null);
|
|
@@ -148,12 +155,13 @@ const App = {
|
|
|
148
155
|
workDirHistory, workDirHistoryOpen,
|
|
149
156
|
});
|
|
150
157
|
|
|
151
|
-
const { connect, wsSend, closeWs } = createConnection({
|
|
158
|
+
const { connect, wsSend, closeWs, submitPassword } = createConnection({
|
|
152
159
|
status, agentName, hostname, workDir, sessionId, error,
|
|
153
160
|
serverVersion, agentVersion,
|
|
154
161
|
messages, isProcessing, isCompacting, visibleLimit,
|
|
155
162
|
historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
|
|
156
163
|
folderPickerLoading, folderPickerEntries, folderPickerPath,
|
|
164
|
+
authRequired, authPassword, authError, authAttempts, authLocked,
|
|
157
165
|
streaming, sidebar, scrollToBottom,
|
|
158
166
|
});
|
|
159
167
|
|
|
@@ -288,6 +296,10 @@ const App = {
|
|
|
288
296
|
selectWorkDirHistory: sidebar.selectWorkDirHistory,
|
|
289
297
|
removeWorkDirHistory: sidebar.removeWorkDirHistory,
|
|
290
298
|
toggleWorkDirHistory: sidebar.toggleWorkDirHistory,
|
|
299
|
+
selectRecentDir: sidebar.selectRecentDir,
|
|
300
|
+
// Authentication
|
|
301
|
+
authRequired, authPassword, authError, authAttempts, authLocked,
|
|
302
|
+
submitPassword,
|
|
291
303
|
// File attachments
|
|
292
304
|
attachments, fileInputRef, dragOver,
|
|
293
305
|
triggerFileInput: fileAttach.triggerFileInput,
|
|
@@ -345,33 +357,11 @@ const App = {
|
|
|
345
357
|
</div>
|
|
346
358
|
<div class="sidebar-workdir-header">
|
|
347
359
|
<div class="sidebar-workdir-label">Working Directory</div>
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
|
|
351
|
-
</button>
|
|
352
|
-
<button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory" :disabled="isProcessing">
|
|
353
|
-
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
|
|
354
|
-
</button>
|
|
355
|
-
</div>
|
|
360
|
+
<button class="sidebar-change-dir-btn" @click="openFolderPicker" title="Change working directory" :disabled="isProcessing">
|
|
361
|
+
<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
|
|
362
|
+
</button>
|
|
356
363
|
</div>
|
|
357
364
|
<div class="sidebar-workdir-path" :title="workDir">{{ workDir }}</div>
|
|
358
|
-
<!-- Workdir history dropdown -->
|
|
359
|
-
<div v-if="workDirHistoryOpen && workDirHistory.length > 1" class="workdir-history-dropdown">
|
|
360
|
-
<div
|
|
361
|
-
v-for="dir in workDirHistory" :key="dir"
|
|
362
|
-
:class="['workdir-history-item', { active: dir === workDir }]"
|
|
363
|
-
@click="selectWorkDirHistory(dir)"
|
|
364
|
-
:title="dir"
|
|
365
|
-
>
|
|
366
|
-
<span class="workdir-history-path">{{ dir }}</span>
|
|
367
|
-
<button
|
|
368
|
-
v-if="dir !== workDir"
|
|
369
|
-
class="workdir-history-remove"
|
|
370
|
-
@click.stop="removeWorkDirHistory(dir)"
|
|
371
|
-
title="Remove from history"
|
|
372
|
-
>×</button>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
365
|
</div>
|
|
376
366
|
</div>
|
|
377
367
|
|
|
@@ -642,6 +632,23 @@ const App = {
|
|
|
642
632
|
</button>
|
|
643
633
|
<input class="folder-picker-path-input" type="text" v-model="folderPickerPath" @keydown.enter="folderPickerGoToPath" placeholder="Enter path..." spellcheck="false" />
|
|
644
634
|
</div>
|
|
635
|
+
<div v-if="workDirHistory.length > 0" class="folder-picker-recent">
|
|
636
|
+
<div class="folder-picker-recent-label">Recent</div>
|
|
637
|
+
<div
|
|
638
|
+
v-for="dir in workDirHistory" :key="dir"
|
|
639
|
+
:class="['folder-picker-recent-item', { active: dir === workDir }]"
|
|
640
|
+
@click="selectRecentDir(dir)"
|
|
641
|
+
:title="dir"
|
|
642
|
+
>
|
|
643
|
+
<svg viewBox="0 0 24 24" width="13" height="13"><path fill="currentColor" d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>
|
|
644
|
+
<span class="folder-picker-recent-path">{{ dir }}</span>
|
|
645
|
+
<button
|
|
646
|
+
class="folder-picker-recent-remove"
|
|
647
|
+
@click.stop="removeWorkDirHistory(dir)"
|
|
648
|
+
title="Remove from history"
|
|
649
|
+
>×</button>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
645
652
|
<div class="folder-picker-list">
|
|
646
653
|
<div v-if="folderPickerLoading" class="folder-picker-loading">
|
|
647
654
|
<div class="history-loading-spinner"></div>
|
|
@@ -682,6 +689,46 @@ const App = {
|
|
|
682
689
|
</div>
|
|
683
690
|
</div>
|
|
684
691
|
</div>
|
|
692
|
+
|
|
693
|
+
<!-- Password Authentication Dialog -->
|
|
694
|
+
<div class="folder-picker-overlay" v-if="authRequired && !authLocked">
|
|
695
|
+
<div class="auth-dialog">
|
|
696
|
+
<div class="auth-dialog-header">
|
|
697
|
+
<svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>
|
|
698
|
+
<span>Session Protected</span>
|
|
699
|
+
</div>
|
|
700
|
+
<div class="auth-dialog-body">
|
|
701
|
+
<p>This session requires a password to access.</p>
|
|
702
|
+
<input
|
|
703
|
+
type="password"
|
|
704
|
+
class="auth-password-input"
|
|
705
|
+
v-model="authPassword"
|
|
706
|
+
@keydown.enter="submitPassword"
|
|
707
|
+
placeholder="Enter password..."
|
|
708
|
+
autofocus
|
|
709
|
+
/>
|
|
710
|
+
<p v-if="authError" class="auth-error">{{ authError }}</p>
|
|
711
|
+
<p v-if="authAttempts" class="auth-attempts">{{ authAttempts }}</p>
|
|
712
|
+
</div>
|
|
713
|
+
<div class="auth-dialog-footer">
|
|
714
|
+
<button class="auth-submit-btn" @click="submitPassword" :disabled="!authPassword.trim()">Unlock</button>
|
|
715
|
+
</div>
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<!-- Auth Locked Out -->
|
|
720
|
+
<div class="folder-picker-overlay" v-if="authLocked">
|
|
721
|
+
<div class="auth-dialog auth-dialog-locked">
|
|
722
|
+
<div class="auth-dialog-header">
|
|
723
|
+
<svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z"/></svg>
|
|
724
|
+
<span>Access Locked</span>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="auth-dialog-body">
|
|
727
|
+
<p>{{ authError }}</p>
|
|
728
|
+
<p class="auth-locked-hint">Close this tab and try again later.</p>
|
|
729
|
+
</div>
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
685
732
|
</div>
|
|
686
733
|
`
|
|
687
734
|
};
|
|
@@ -17,6 +17,7 @@ export function createConnection(deps) {
|
|
|
17
17
|
messages, isProcessing, isCompacting, visibleLimit,
|
|
18
18
|
historySessions, currentClaudeSessionId, loadingSessions, loadingHistory,
|
|
19
19
|
folderPickerLoading, folderPickerEntries, folderPickerPath,
|
|
20
|
+
authRequired, authPassword, authError, authAttempts, authLocked,
|
|
20
21
|
streaming, sidebar,
|
|
21
22
|
scrollToBottom,
|
|
22
23
|
} = deps;
|
|
@@ -113,7 +114,12 @@ export function createConnection(deps) {
|
|
|
113
114
|
error.value = '';
|
|
114
115
|
|
|
115
116
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
116
|
-
|
|
117
|
+
let wsUrl = `${protocol}//${window.location.host}/?type=web&sessionId=${sid}`;
|
|
118
|
+
// Include saved auth token for automatic re-authentication
|
|
119
|
+
const savedToken = localStorage.getItem(`agentlink-auth-${sid}`);
|
|
120
|
+
if (savedToken) {
|
|
121
|
+
wsUrl += `&authToken=${encodeURIComponent(savedToken)}`;
|
|
122
|
+
}
|
|
117
123
|
ws = new WebSocket(wsUrl);
|
|
118
124
|
|
|
119
125
|
ws.onopen = () => { error.value = ''; reconnectAttempts = 0; };
|
|
@@ -122,6 +128,30 @@ export function createConnection(deps) {
|
|
|
122
128
|
let msg;
|
|
123
129
|
const parsed = JSON.parse(event.data);
|
|
124
130
|
|
|
131
|
+
// Auth messages are always plaintext (before session key exchange)
|
|
132
|
+
if (parsed.type === 'auth_required') {
|
|
133
|
+
authRequired.value = true;
|
|
134
|
+
authError.value = '';
|
|
135
|
+
authLocked.value = false;
|
|
136
|
+
status.value = 'Authentication Required';
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (parsed.type === 'auth_failed') {
|
|
140
|
+
authError.value = parsed.message || 'Incorrect password.';
|
|
141
|
+
authAttempts.value = parsed.attemptsRemaining != null
|
|
142
|
+
? `${parsed.attemptsRemaining} attempt${parsed.attemptsRemaining !== 1 ? 's' : ''} remaining`
|
|
143
|
+
: null;
|
|
144
|
+
authPassword.value = '';
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (parsed.type === 'auth_locked') {
|
|
148
|
+
authLocked.value = true;
|
|
149
|
+
authRequired.value = false;
|
|
150
|
+
authError.value = parsed.message || 'Too many failed attempts.';
|
|
151
|
+
status.value = 'Locked';
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
if (parsed.type === 'connected') {
|
|
126
156
|
msg = parsed;
|
|
127
157
|
if (typeof parsed.sessionKey === 'string') {
|
|
@@ -138,6 +168,16 @@ export function createConnection(deps) {
|
|
|
138
168
|
}
|
|
139
169
|
|
|
140
170
|
if (msg.type === 'connected') {
|
|
171
|
+
// Reset auth state
|
|
172
|
+
authRequired.value = false;
|
|
173
|
+
authPassword.value = '';
|
|
174
|
+
authError.value = '';
|
|
175
|
+
authAttempts.value = null;
|
|
176
|
+
authLocked.value = false;
|
|
177
|
+
// Save auth token for automatic re-authentication
|
|
178
|
+
if (msg.authToken) {
|
|
179
|
+
localStorage.setItem(`agentlink-auth-${sessionId.value}`, msg.authToken);
|
|
180
|
+
}
|
|
141
181
|
if (msg.serverVersion) serverVersion.value = msg.serverVersion;
|
|
142
182
|
if (msg.agent) {
|
|
143
183
|
status.value = 'Connected';
|
|
@@ -366,6 +406,9 @@ export function createConnection(deps) {
|
|
|
366
406
|
isProcessing.value = false;
|
|
367
407
|
isCompacting.value = false;
|
|
368
408
|
|
|
409
|
+
// Don't auto-reconnect if auth-locked or still in auth prompt
|
|
410
|
+
if (authLocked.value || authRequired.value) return;
|
|
411
|
+
|
|
369
412
|
if (wasConnected || reconnectAttempts > 0) {
|
|
370
413
|
scheduleReconnect(scheduleHighlight);
|
|
371
414
|
}
|
|
@@ -393,5 +436,12 @@ export function createConnection(deps) {
|
|
|
393
436
|
if (ws) ws.close();
|
|
394
437
|
}
|
|
395
438
|
|
|
396
|
-
|
|
439
|
+
function submitPassword() {
|
|
440
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
441
|
+
const pwd = authPassword.value.trim();
|
|
442
|
+
if (!pwd) return;
|
|
443
|
+
ws.send(JSON.stringify({ type: 'authenticate', password: pwd }));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return { connect, wsSend, closeWs, submitPassword };
|
|
397
447
|
}
|
package/web/modules/sidebar.js
CHANGED
|
@@ -59,6 +59,15 @@ export function createSidebar(deps) {
|
|
|
59
59
|
wsSend({ type: 'change_workdir', workDir: dir });
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function selectRecentDir(dir) {
|
|
63
|
+
if (dir === workDir.value) {
|
|
64
|
+
folderPickerOpen.value = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
folderPickerOpen.value = false;
|
|
68
|
+
wsSend({ type: 'change_workdir', workDir: dir });
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
function removeWorkDirHistory(dir) {
|
|
63
72
|
const list = workDirHistory.value.filter(d => d !== dir);
|
|
64
73
|
workDirHistory.value = list;
|
|
@@ -246,6 +255,7 @@ export function createSidebar(deps) {
|
|
|
246
255
|
requestSessionList, resumeSession, newConversation, toggleSidebar,
|
|
247
256
|
deleteSession, confirmDeleteSession, cancelDeleteSession,
|
|
248
257
|
addToWorkDirHistory, selectWorkDirHistory, removeWorkDirHistory, toggleWorkDirHistory,
|
|
258
|
+
selectRecentDir,
|
|
249
259
|
openFolderPicker, folderPickerNavigateUp, folderPickerSelectItem,
|
|
250
260
|
folderPickerEnter, folderPickerGoToPath, confirmFolderPicker,
|
|
251
261
|
groupedSessions,
|
package/web/style.css
CHANGED
|
@@ -534,6 +534,104 @@ body {
|
|
|
534
534
|
background: #dc2626;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
+
/* ── Auth Dialog ── */
|
|
538
|
+
.auth-dialog {
|
|
539
|
+
background: var(--bg-secondary);
|
|
540
|
+
border: 1px solid var(--border);
|
|
541
|
+
border-radius: 12px;
|
|
542
|
+
width: 360px;
|
|
543
|
+
max-width: 90vw;
|
|
544
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.auth-dialog-header {
|
|
548
|
+
display: flex;
|
|
549
|
+
align-items: center;
|
|
550
|
+
gap: 10px;
|
|
551
|
+
padding: 20px 24px 16px;
|
|
552
|
+
font-size: 1rem;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: var(--text-primary);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.auth-dialog-header svg {
|
|
558
|
+
color: var(--accent);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.auth-dialog-locked .auth-dialog-header svg {
|
|
562
|
+
color: var(--error);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.auth-dialog-body {
|
|
566
|
+
padding: 0 24px 16px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.auth-dialog-body p {
|
|
570
|
+
margin: 0 0 12px 0;
|
|
571
|
+
color: var(--text-secondary);
|
|
572
|
+
font-size: 0.85rem;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.auth-password-input {
|
|
576
|
+
width: 100%;
|
|
577
|
+
padding: 10px 12px;
|
|
578
|
+
background: var(--bg-tertiary);
|
|
579
|
+
border: 1px solid var(--border);
|
|
580
|
+
border-radius: 8px;
|
|
581
|
+
color: var(--text-primary);
|
|
582
|
+
font-size: 0.9rem;
|
|
583
|
+
outline: none;
|
|
584
|
+
box-sizing: border-box;
|
|
585
|
+
margin-bottom: 4px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.auth-password-input:focus {
|
|
589
|
+
border-color: var(--accent);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.auth-error {
|
|
593
|
+
color: var(--error) !important;
|
|
594
|
+
font-size: 0.82rem !important;
|
|
595
|
+
margin-top: 8px !important;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.auth-attempts {
|
|
599
|
+
color: var(--text-secondary) !important;
|
|
600
|
+
font-size: 0.78rem !important;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.auth-locked-hint {
|
|
604
|
+
font-size: 0.78rem !important;
|
|
605
|
+
color: var(--text-secondary) !important;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
.auth-dialog-footer {
|
|
609
|
+
padding: 12px 24px 20px;
|
|
610
|
+
display: flex;
|
|
611
|
+
justify-content: flex-end;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.auth-submit-btn {
|
|
615
|
+
background: var(--accent);
|
|
616
|
+
color: #fff;
|
|
617
|
+
border: none;
|
|
618
|
+
padding: 8px 24px;
|
|
619
|
+
border-radius: 8px;
|
|
620
|
+
font-size: 0.85rem;
|
|
621
|
+
font-weight: 600;
|
|
622
|
+
cursor: pointer;
|
|
623
|
+
transition: background 0.15s;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.auth-submit-btn:hover:not(:disabled) {
|
|
627
|
+
background: var(--accent-hover);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.auth-submit-btn:disabled {
|
|
631
|
+
opacity: 0.4;
|
|
632
|
+
cursor: not-allowed;
|
|
633
|
+
}
|
|
634
|
+
|
|
537
635
|
/* ── Chat area (message list + input) ── */
|
|
538
636
|
.chat-area {
|
|
539
637
|
flex: 1;
|
|
@@ -1543,79 +1641,6 @@ body {
|
|
|
1543
1641
|
cursor: not-allowed;
|
|
1544
1642
|
}
|
|
1545
1643
|
|
|
1546
|
-
.sidebar-workdir-actions {
|
|
1547
|
-
display: flex;
|
|
1548
|
-
gap: 4px;
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
/* ── Workdir history dropdown ── */
|
|
1552
|
-
.workdir-history-dropdown {
|
|
1553
|
-
margin-top: 6px;
|
|
1554
|
-
border: 1px solid var(--border);
|
|
1555
|
-
border-radius: 6px;
|
|
1556
|
-
background: var(--bg-secondary);
|
|
1557
|
-
max-height: 200px;
|
|
1558
|
-
overflow-y: auto;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
.workdir-history-item {
|
|
1562
|
-
display: flex;
|
|
1563
|
-
align-items: center;
|
|
1564
|
-
padding: 6px 8px;
|
|
1565
|
-
font-size: 0.75rem;
|
|
1566
|
-
font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
|
|
1567
|
-
color: var(--text-primary);
|
|
1568
|
-
cursor: pointer;
|
|
1569
|
-
transition: background 0.1s;
|
|
1570
|
-
gap: 4px;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
.workdir-history-item:hover {
|
|
1574
|
-
background: var(--bg-tertiary);
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
.workdir-history-item.active {
|
|
1578
|
-
color: var(--accent);
|
|
1579
|
-
font-weight: 600;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
.workdir-history-item + .workdir-history-item {
|
|
1583
|
-
border-top: 1px solid var(--border);
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
.workdir-history-path {
|
|
1587
|
-
flex: 1;
|
|
1588
|
-
overflow: hidden;
|
|
1589
|
-
text-overflow: ellipsis;
|
|
1590
|
-
white-space: nowrap;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
.workdir-history-remove {
|
|
1594
|
-
flex-shrink: 0;
|
|
1595
|
-
display: none;
|
|
1596
|
-
align-items: center;
|
|
1597
|
-
justify-content: center;
|
|
1598
|
-
width: 18px;
|
|
1599
|
-
height: 18px;
|
|
1600
|
-
background: none;
|
|
1601
|
-
border: none;
|
|
1602
|
-
border-radius: 3px;
|
|
1603
|
-
color: var(--text-secondary);
|
|
1604
|
-
font-size: 0.85rem;
|
|
1605
|
-
cursor: pointer;
|
|
1606
|
-
padding: 0;
|
|
1607
|
-
line-height: 1;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
.workdir-history-item:hover .workdir-history-remove {
|
|
1611
|
-
display: flex;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
.workdir-history-remove:hover {
|
|
1615
|
-
color: var(--error);
|
|
1616
|
-
background: rgba(239, 68, 68, 0.1);
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
1644
|
/* ── Folder Picker Modal ── */
|
|
1620
1645
|
.folder-picker-overlay {
|
|
1621
1646
|
position: fixed;
|
|
@@ -1820,6 +1845,77 @@ body {
|
|
|
1820
1845
|
cursor: not-allowed;
|
|
1821
1846
|
}
|
|
1822
1847
|
|
|
1848
|
+
/* ── Folder Picker: Recent directories ── */
|
|
1849
|
+
.folder-picker-recent {
|
|
1850
|
+
border-bottom: 1px solid var(--border);
|
|
1851
|
+
padding: 6px 0;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.folder-picker-recent-label {
|
|
1855
|
+
padding: 4px 16px 2px;
|
|
1856
|
+
font-size: 0.7rem;
|
|
1857
|
+
font-weight: 600;
|
|
1858
|
+
text-transform: uppercase;
|
|
1859
|
+
letter-spacing: 0.04em;
|
|
1860
|
+
color: var(--text-secondary);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
.folder-picker-recent-item {
|
|
1864
|
+
display: flex;
|
|
1865
|
+
align-items: center;
|
|
1866
|
+
gap: 8px;
|
|
1867
|
+
padding: 5px 16px;
|
|
1868
|
+
font-size: 0.82rem;
|
|
1869
|
+
cursor: pointer;
|
|
1870
|
+
color: var(--text-primary);
|
|
1871
|
+
transition: background 0.1s;
|
|
1872
|
+
user-select: none;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
.folder-picker-recent-item:hover {
|
|
1876
|
+
background: var(--bg-tertiary);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
.folder-picker-recent-item.active {
|
|
1880
|
+
color: var(--accent);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
.folder-picker-recent-item svg {
|
|
1884
|
+
flex-shrink: 0;
|
|
1885
|
+
color: var(--text-secondary);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
.folder-picker-recent-item.active svg {
|
|
1889
|
+
color: var(--accent);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
.folder-picker-recent-path {
|
|
1893
|
+
flex: 1;
|
|
1894
|
+
overflow: hidden;
|
|
1895
|
+
text-overflow: ellipsis;
|
|
1896
|
+
white-space: nowrap;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
.folder-picker-recent-remove {
|
|
1900
|
+
background: none;
|
|
1901
|
+
border: none;
|
|
1902
|
+
color: var(--text-secondary);
|
|
1903
|
+
font-size: 1rem;
|
|
1904
|
+
cursor: pointer;
|
|
1905
|
+
padding: 0 4px;
|
|
1906
|
+
line-height: 1;
|
|
1907
|
+
opacity: 0;
|
|
1908
|
+
transition: opacity 0.15s, color 0.15s;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
.folder-picker-recent-item:hover .folder-picker-recent-remove {
|
|
1912
|
+
opacity: 1;
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
.folder-picker-recent-remove:hover {
|
|
1916
|
+
color: var(--error);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1823
1919
|
/* ── File Upload: Attachment Bar ── */
|
|
1824
1920
|
.attachment-bar {
|
|
1825
1921
|
display: flex;
|