@agent-link/server 0.1.160 → 0.1.161
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/package.json +1 -1
- package/web/app.js +263 -238
- package/web/locales/en.json +250 -0
- package/web/locales/zh.json +250 -0
- package/web/modules/appHelpers.js +4 -2
- package/web/modules/connection.js +17 -15
- package/web/modules/i18n.js +101 -0
- package/web/modules/messageHelpers.js +27 -20
- package/web/modules/sidebar.js +19 -10
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// ── Lightweight i18n module ─────────────────────────────────────────────────
|
|
2
|
+
const { ref, computed } = Vue;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates i18n functionality: t() translator, locale switching, persistence.
|
|
6
|
+
* Locale data is loaded dynamically from /locales/<lang>.json.
|
|
7
|
+
*
|
|
8
|
+
* @returns {{ t: Function, locale: import('vue').Ref<string>, setLocale: Function }}
|
|
9
|
+
*/
|
|
10
|
+
export function createI18n() {
|
|
11
|
+
const STORAGE_KEY = 'agentlink-language';
|
|
12
|
+
const SUPPORTED = ['en', 'zh'];
|
|
13
|
+
const DEFAULT_LOCALE = 'en';
|
|
14
|
+
|
|
15
|
+
// Detect initial locale
|
|
16
|
+
function detectLocale() {
|
|
17
|
+
// 1. Explicit user choice
|
|
18
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
19
|
+
if (stored && SUPPORTED.includes(stored)) return stored;
|
|
20
|
+
|
|
21
|
+
// 2. Browser preference
|
|
22
|
+
const nav = (navigator.language || '').toLowerCase();
|
|
23
|
+
if (nav.startsWith('zh')) return 'zh';
|
|
24
|
+
|
|
25
|
+
// 3. Default
|
|
26
|
+
return DEFAULT_LOCALE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const locale = ref(detectLocale());
|
|
30
|
+
const _messages = ref({});
|
|
31
|
+
let _loadedLocale = null;
|
|
32
|
+
|
|
33
|
+
// Load locale JSON
|
|
34
|
+
async function loadMessages(lang) {
|
|
35
|
+
if (_loadedLocale === lang && Object.keys(_messages.value).length > 0) return;
|
|
36
|
+
try {
|
|
37
|
+
const resp = await fetch(`/locales/${lang}.json`);
|
|
38
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
39
|
+
_messages.value = await resp.json();
|
|
40
|
+
_loadedLocale = lang;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn(`[i18n] Failed to load locale "${lang}":`, e);
|
|
43
|
+
// Fallback: try loading English
|
|
44
|
+
if (lang !== DEFAULT_LOCALE) {
|
|
45
|
+
try {
|
|
46
|
+
const resp = await fetch(`/locales/${DEFAULT_LOCALE}.json`);
|
|
47
|
+
if (resp.ok) {
|
|
48
|
+
_messages.value = await resp.json();
|
|
49
|
+
_loadedLocale = DEFAULT_LOCALE;
|
|
50
|
+
}
|
|
51
|
+
} catch { /* give up */ }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Translate a key, with optional parameter substitution.
|
|
58
|
+
* Returns the key itself if no translation is found (fallback).
|
|
59
|
+
*
|
|
60
|
+
* @param {string} key - Dot-notation key, e.g. "button.send"
|
|
61
|
+
* @param {object} [params] - Substitution params, e.g. { n: 5 }
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function t(key, params) {
|
|
65
|
+
let str = _messages.value[key];
|
|
66
|
+
if (str === undefined) return key;
|
|
67
|
+
if (params) {
|
|
68
|
+
for (const [k, v] of Object.entries(params)) {
|
|
69
|
+
str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return str;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Switch locale, persist choice, and reload strings.
|
|
77
|
+
* @param {string} lang
|
|
78
|
+
*/
|
|
79
|
+
async function setLocale(lang) {
|
|
80
|
+
if (!SUPPORTED.includes(lang)) return;
|
|
81
|
+
locale.value = lang;
|
|
82
|
+
localStorage.setItem(STORAGE_KEY, lang);
|
|
83
|
+
await loadMessages(lang);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Toggle between supported locales (EN ↔ 中).
|
|
88
|
+
*/
|
|
89
|
+
async function toggleLocale() {
|
|
90
|
+
const next = locale.value === 'en' ? 'zh' : 'en';
|
|
91
|
+
await setLocale(next);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The display label for the language switcher button
|
|
95
|
+
const localeLabel = computed(() => locale.value === 'en' ? 'EN' : '中');
|
|
96
|
+
|
|
97
|
+
// Load initial messages
|
|
98
|
+
loadMessages(locale.value);
|
|
99
|
+
|
|
100
|
+
return { t, locale, setLocale, toggleLocale, localeLabel };
|
|
101
|
+
}
|
|
@@ -8,15 +8,15 @@ export function isContextSummary(text) {
|
|
|
8
8
|
return typeof text === 'string' && text.trimStart().startsWith(CONTEXT_SUMMARY_PREFIX);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function formatRelativeTime(ts) {
|
|
11
|
+
export function formatRelativeTime(ts, t) {
|
|
12
12
|
const diff = Date.now() - ts;
|
|
13
13
|
const mins = Math.floor(diff / 60000);
|
|
14
|
-
if (mins < 1) return 'just now';
|
|
15
|
-
if (mins < 60) return `${mins}m ago`;
|
|
14
|
+
if (mins < 1) return t ? t('time.justNow') : 'just now';
|
|
15
|
+
if (mins < 60) return t ? t('time.minutesAgo', { n: mins }) : `${mins}m ago`;
|
|
16
16
|
const hours = Math.floor(mins / 60);
|
|
17
|
-
if (hours < 24) return `${hours}h ago`;
|
|
17
|
+
if (hours < 24) return t ? t('time.hoursAgo', { n: hours }) : `${hours}h ago`;
|
|
18
18
|
const days = Math.floor(hours / 24);
|
|
19
|
-
if (days < 30) return `${days}d ago`;
|
|
19
|
+
if (days < 30) return t ? t('time.daysAgo', { n: days }) : `${days}d ago`;
|
|
20
20
|
return new Date(ts).toLocaleDateString();
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -53,7 +53,7 @@ export function toggleTool(msg) {
|
|
|
53
53
|
msg.expanded = !msg.expanded;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export function getToolSummary(msg) {
|
|
56
|
+
export function getToolSummary(msg, t) {
|
|
57
57
|
const name = msg.toolName;
|
|
58
58
|
const input = msg.toolInput;
|
|
59
59
|
try {
|
|
@@ -65,8 +65,8 @@ export function getToolSummary(msg) {
|
|
|
65
65
|
if (name === 'Glob' && obj.pattern) return obj.pattern;
|
|
66
66
|
if (name === 'Grep' && obj.pattern) return obj.pattern;
|
|
67
67
|
if (name === 'TodoWrite' && obj.todos) {
|
|
68
|
-
const
|
|
69
|
-
return
|
|
68
|
+
const doneCount = obj.todos.filter(td => td.status === 'completed').length;
|
|
69
|
+
return t ? t('tool.done', { done: doneCount, total: obj.todos.length }) : `${doneCount}/${obj.todos.length} done`;
|
|
70
70
|
}
|
|
71
71
|
if (name === 'Task' && obj.description) return obj.description;
|
|
72
72
|
if (name === 'Agent' && obj.description) return obj.description;
|
|
@@ -81,7 +81,7 @@ export function isEditTool(msg) {
|
|
|
81
81
|
return msg.role === 'tool' && msg.toolName === 'Edit' && msg.toolInput;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function getFormattedToolInput(msg) {
|
|
84
|
+
export function getFormattedToolInput(msg, t) {
|
|
85
85
|
if (!msg.toolInput) return null;
|
|
86
86
|
try {
|
|
87
87
|
const obj = JSON.parse(msg.toolInput);
|
|
@@ -91,18 +91,22 @@ export function getFormattedToolInput(msg) {
|
|
|
91
91
|
if (name === 'Read' && obj.file_path) {
|
|
92
92
|
let detail = esc(obj.file_path);
|
|
93
93
|
if (obj.offset && obj.limit) {
|
|
94
|
-
|
|
94
|
+
const meta = t ? t('tool.lines', { start: obj.offset, end: obj.offset + obj.limit - 1 }) : `lines ${obj.offset}\u2013${obj.offset + obj.limit - 1}`;
|
|
95
|
+
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
95
96
|
} else if (obj.offset) {
|
|
96
|
-
|
|
97
|
+
const meta = t ? t('tool.fromLine', { offset: obj.offset }) : `from line ${obj.offset}`;
|
|
98
|
+
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
97
99
|
} else if (obj.limit) {
|
|
98
|
-
|
|
100
|
+
const meta = t ? t('tool.firstLines', { limit: obj.limit }) : `first ${obj.limit} lines`;
|
|
101
|
+
detail += ` <span class="tool-input-meta">${meta}</span>`;
|
|
99
102
|
}
|
|
100
103
|
return detail;
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
if (name === 'Write' && obj.file_path) {
|
|
104
107
|
const lines = (obj.content || '').split('\n').length;
|
|
105
|
-
|
|
108
|
+
const lineCount = t ? t('tool.lineCount', { n: lines }) : `${lines} lines`;
|
|
109
|
+
return esc(obj.file_path) + ` <span class="tool-input-meta">${lineCount}</span>`;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
if (name === 'Bash' && obj.command) {
|
|
@@ -113,13 +117,13 @@ export function getFormattedToolInput(msg) {
|
|
|
113
117
|
|
|
114
118
|
if (name === 'Glob' && obj.pattern) {
|
|
115
119
|
let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
|
|
116
|
-
if (obj.path) html += ' <span class="tool-input-meta">in ' + esc(obj.path) + '</span>';
|
|
120
|
+
if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
|
|
117
121
|
return html;
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if (name === 'Grep' && obj.pattern) {
|
|
121
125
|
let html = '<code class="tool-input-cmd">' + esc(obj.pattern) + '</code>';
|
|
122
|
-
if (obj.path) html += ' <span class="tool-input-meta">in ' + esc(obj.path) + '</span>';
|
|
126
|
+
if (obj.path) html += ' <span class="tool-input-meta">' + (t ? t('tool.inPath', { path: esc(obj.path) }) : 'in ' + esc(obj.path)) + '</span>';
|
|
123
127
|
return html;
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -139,10 +143,13 @@ export function getFormattedToolInput(msg) {
|
|
|
139
143
|
|
|
140
144
|
if (name === 'Task' || name === 'Agent') {
|
|
141
145
|
let html = '';
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
const descLabel = t ? t('tool.description') : 'Description';
|
|
147
|
+
const agentLabel = t ? t('tool.agent') : 'Agent';
|
|
148
|
+
const promptLabel = t ? t('tool.prompt') : 'Prompt';
|
|
149
|
+
if (obj.description) html += '<div class="task-field"><span class="tool-input-meta">' + descLabel + '</span> ' + esc(obj.description) + '</div>';
|
|
150
|
+
if (obj.subagent_type) html += '<div class="task-field"><span class="tool-input-meta">' + agentLabel + '</span> <code class="tool-input-cmd">' + esc(obj.subagent_type) + '</code></div>';
|
|
144
151
|
if (obj.prompt) {
|
|
145
|
-
html += '<div class="task-field"><span class="tool-input-meta">
|
|
152
|
+
html += '<div class="task-field"><span class="tool-input-meta">' + promptLabel + '</span></div><div class="task-prompt">' + esc(obj.prompt) + '</div>';
|
|
146
153
|
}
|
|
147
154
|
if (html) return html;
|
|
148
155
|
}
|
|
@@ -161,7 +168,7 @@ export function getFormattedToolInput(msg) {
|
|
|
161
168
|
return null;
|
|
162
169
|
}
|
|
163
170
|
|
|
164
|
-
export function getEditDiffHtml(msg) {
|
|
171
|
+
export function getEditDiffHtml(msg, t) {
|
|
165
172
|
try {
|
|
166
173
|
const obj = JSON.parse(msg.toolInput);
|
|
167
174
|
if (!obj.old_string && !obj.new_string) return null;
|
|
@@ -171,7 +178,7 @@ export function getEditDiffHtml(msg) {
|
|
|
171
178
|
const newLines = (obj.new_string || '').split('\n');
|
|
172
179
|
let html = '';
|
|
173
180
|
if (filePath) {
|
|
174
|
-
html += '<div class="diff-file">' + esc(filePath) + (obj.replace_all ? ' <span class="diff-replace-all">(replace all)</span>' : '') + '</div>';
|
|
181
|
+
html += '<div class="diff-file">' + esc(filePath) + (obj.replace_all ? ' <span class="diff-replace-all">' + (t ? t('tool.replaceAll') : '(replace all)') + '</span>' : '') + '</div>';
|
|
175
182
|
}
|
|
176
183
|
html += '<div class="diff-lines">';
|
|
177
184
|
for (const line of oldLines) {
|
package/web/modules/sidebar.js
CHANGED
|
@@ -35,6 +35,8 @@ export function createSidebar(deps) {
|
|
|
35
35
|
// Multi-session parallel
|
|
36
36
|
currentConversationId, conversationCache, processingConversations,
|
|
37
37
|
switchConversation,
|
|
38
|
+
// i18n
|
|
39
|
+
t,
|
|
38
40
|
} = deps;
|
|
39
41
|
|
|
40
42
|
// Late-binding callback: called when user switches to a normal chat session
|
|
@@ -130,7 +132,7 @@ export function createSidebar(deps) {
|
|
|
130
132
|
switchConversation(newConvId);
|
|
131
133
|
messages.value.push({
|
|
132
134
|
id: streaming.nextId(), role: 'system',
|
|
133
|
-
content: '
|
|
135
|
+
content: t('system.newConversation'),
|
|
134
136
|
timestamp: new Date(),
|
|
135
137
|
});
|
|
136
138
|
return;
|
|
@@ -365,18 +367,25 @@ export function createSidebar(deps) {
|
|
|
365
367
|
const yesterdayStart = todayStart - 86400000;
|
|
366
368
|
const weekStart = todayStart - 6 * 86400000;
|
|
367
369
|
|
|
370
|
+
const GROUP_KEYS = {
|
|
371
|
+
today: 'session.today',
|
|
372
|
+
yesterday: 'session.yesterday',
|
|
373
|
+
thisWeek: 'session.thisWeek',
|
|
374
|
+
earlier: 'session.earlier',
|
|
375
|
+
};
|
|
376
|
+
|
|
368
377
|
const groups = {};
|
|
369
378
|
for (const s of historySessions.value) {
|
|
370
|
-
let
|
|
371
|
-
if (s.lastModified >= todayStart)
|
|
372
|
-
else if (s.lastModified >= yesterdayStart)
|
|
373
|
-
else if (s.lastModified >= weekStart)
|
|
374
|
-
else
|
|
375
|
-
if (!groups[
|
|
376
|
-
groups[
|
|
379
|
+
let key;
|
|
380
|
+
if (s.lastModified >= todayStart) key = 'today';
|
|
381
|
+
else if (s.lastModified >= yesterdayStart) key = 'yesterday';
|
|
382
|
+
else if (s.lastModified >= weekStart) key = 'thisWeek';
|
|
383
|
+
else key = 'earlier';
|
|
384
|
+
if (!groups[key]) groups[key] = [];
|
|
385
|
+
groups[key].push(s);
|
|
377
386
|
}
|
|
378
|
-
const order = ['
|
|
379
|
-
return order.filter(k => groups[k]).map(k => ({ label: k, sessions: groups[k] }));
|
|
387
|
+
const order = ['today', 'yesterday', 'thisWeek', 'earlier'];
|
|
388
|
+
return order.filter(k => groups[k]).map(k => ({ label: t(GROUP_KEYS[k]), sessions: groups[k] }));
|
|
380
389
|
});
|
|
381
390
|
|
|
382
391
|
return {
|