@songsid/agend 2.3.2-beta.11 → 2.3.2-beta.13

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.
Files changed (50) hide show
  1. package/README.md +4 -0
  2. package/README.zh-TW.md +4 -0
  3. package/dist/auth-api.js +2 -0
  4. package/dist/auth-api.js.map +1 -1
  5. package/dist/backend/kiro-identity.d.ts +55 -0
  6. package/dist/backend/kiro-identity.js +218 -28
  7. package/dist/backend/kiro-identity.js.map +1 -1
  8. package/dist/backend/kiro.d.ts +2 -0
  9. package/dist/backend/kiro.js +25 -4
  10. package/dist/backend/kiro.js.map +1 -1
  11. package/dist/fleet-manager.js +2 -0
  12. package/dist/fleet-manager.js.map +1 -1
  13. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  14. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +3 -0
  15. package/dist/general-knowledge/skills/web-appearance/SKILL.md +3 -0
  16. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +15 -1
  17. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +15 -1
  18. package/dist/kiro-migrate.d.ts +5 -0
  19. package/dist/kiro-migrate.js +42 -4
  20. package/dist/kiro-migrate.js.map +1 -1
  21. package/dist/locale.js +28 -0
  22. package/dist/locale.js.map +1 -1
  23. package/dist/quickstart-api.d.ts +14 -0
  24. package/dist/quickstart-api.js +62 -11
  25. package/dist/quickstart-api.js.map +1 -1
  26. package/dist/settings-effect.js +5 -4
  27. package/dist/settings-effect.js.map +1 -1
  28. package/dist/setup-form.d.ts +2 -2
  29. package/dist/setup-form.js +124 -92
  30. package/dist/setup-form.js.map +1 -1
  31. package/dist/setup-i18n.d.ts +25 -0
  32. package/dist/setup-i18n.js +224 -0
  33. package/dist/setup-i18n.js.map +1 -0
  34. package/dist/ui/backend-signin.js +4 -3
  35. package/dist/ui/fleet-office-pack.d.ts +3 -2
  36. package/dist/ui/fleet-office-pack.js +16 -4
  37. package/dist/ui/fleet-office-zones.js +28 -4
  38. package/dist/ui/fleet-office.js +98 -14
  39. package/dist/ui/panel-settings.js +4 -2
  40. package/dist/ui/settings-discord.js +3 -2
  41. package/dist/ui/settings-strings.js +8 -2
  42. package/dist/ui/settings-token.js +3 -2
  43. package/dist/ui/settings-wizard.js +11 -6
  44. package/dist/ui/shared/setup-host-text.js +105 -0
  45. package/dist/ui/structured-signin.js +89 -38
  46. package/dist/web-office-store.d.ts +16 -0
  47. package/dist/web-office-store.js +44 -10
  48. package/dist/web-office-store.js.map +1 -1
  49. package/package.json +1 -1
  50. package/templates/windows-shortcuts.ps1 +18 -17
@@ -1,3 +1,4 @@
1
+ import { SETUP_CODE_STRINGS, SETUP_FORM_STRINGS, setupI18nRuntime } from "./setup-i18n.js";
1
2
  /**
2
3
  * The pre-fleet form, inline.
3
4
  *
@@ -33,27 +34,35 @@ export const SETUP_CODE_PAGE_HTML = `<!DOCTYPE html>
33
34
  button { width: 100%; margin-top: 12px; padding: 12px; border: 1px solid #2563eb; border-radius: 6px; background: #2563eb; color: #fff; font-size: 15px; cursor: pointer; }
34
35
  button:disabled { opacity: .5; cursor: default; }
35
36
  .err { color: #dc2626; font-size: 13px; margin-top: 12px; min-height: 18px; }
37
+ .lang { display: flex; justify-content: flex-end; }
38
+ .lang button { width: auto; margin: 0; padding: 4px 10px; border-color: #e5e7eb; background: #fff; color: #111827; font-size: 13px; }
36
39
  </style>
37
40
  </head>
38
41
  <body>
39
42
  <main>
40
- <h1>Set up AgEnD</h1>
41
- <p>Enter the setup code shown in the terminal where you ran <code>agend setup</code>.</p>
43
+ <div class="lang"><button id="lang" type="button" data-i18n="otherLang" data-i18n-label="langTitle">中文</button></div>
44
+ <h1 data-i18n="title">Set up AgEnD</h1>
45
+ <p id="lead">Enter the setup code shown in the terminal where you ran <code>agend setup</code>.</p>
42
46
  <form id="f">
43
47
  <input id="code" autocomplete="off" autocapitalize="characters" spellcheck="false" placeholder="ABCD-EFGH" maxlength="12" autofocus>
44
- <button id="go" type="submit">Continue</button>
48
+ <button id="go" type="submit" data-i18n="continue">Continue</button>
45
49
  </form>
46
50
  <div class="err" id="msg"></div>
47
51
  </main>
48
52
  <script>
49
53
  (() => {
50
54
  "use strict";
55
+ ${setupI18nRuntime(SETUP_CODE_STRINGS)}
56
+ show(document.getElementById("lead"), el => {
57
+ const [before, after] = tr("lead").split("{0}"), command = document.createElement("code");
58
+ command.textContent = "agend setup"; el.replaceChildren(before, command, after ?? "");
59
+ });
51
60
  const msg = document.getElementById("msg");
52
61
  const go = document.getElementById("go");
53
62
  document.getElementById("f").addEventListener("submit", async (event) => {
54
63
  event.preventDefault();
55
64
  go.disabled = true;
56
- msg.textContent = "";
65
+ show(msg, el => { el.textContent = ""; });
57
66
  let res;
58
67
  try {
59
68
  res = await fetch("open", {
@@ -62,13 +71,14 @@ export const SETUP_CODE_PAGE_HTML = `<!DOCTYPE html>
62
71
  body: JSON.stringify({ code: document.getElementById("code").value }),
63
72
  });
64
73
  } catch {
65
- msg.textContent = "Could not reach the setup page.";
74
+ show(msg, el => { el.textContent = tr("unreachable"); });
66
75
  go.disabled = false;
67
76
  return;
68
77
  }
69
78
  if (res.ok) { location.reload(); return; }
70
79
  let body = null; try { body = await res.json(); } catch {}
71
- msg.textContent = (body && body.error) || "That code was not accepted.";
80
+ const error = body && body.error;
81
+ show(msg, el => { el.textContent = error ? trHost(error) : tr("notAccepted"); });
72
82
  // A spent budget ends the page; there is nothing useful left to type into.
73
83
  if (res.status !== 410) go.disabled = false;
74
84
  });
@@ -100,51 +110,54 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
100
110
  .msg { font-size: 13px; }
101
111
  .ok { color: #059669; } .err { color: #dc2626; }
102
112
  pre { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 12px; font-size: 12px; overflow: auto; }
113
+ .top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
114
+ #lang { padding: 4px 10px; font-size: 13px; flex: none; }
103
115
  </style>
104
116
  </head>
105
117
  <body>
106
118
  <main>
107
- <h1>Set up AgEnD</h1>
108
- <div class="sub" id="intro">This page is open only while setup is running. It closes itself when you finish, or after 15 minutes.</div>
119
+ <div class="top"><h1 data-i18n="title">Set up AgEnD</h1><button id="lang" type="button" data-i18n="otherLang" data-i18n-label="langTitle">中文</button></div>
120
+ <div class="sub" id="intro" data-i18n="intro">This page is open only while setup is running. It closes itself when you finish, or after 15 minutes.</div>
109
121
  <div class="sub msg" id="ttlMsg" role="status"></div>
110
122
 
111
- <div class="step" id="setupSignins" aria-label="AI backend sign-ins"></div>
123
+ <div class="step" id="setupSignins" aria-label="AI backend sign-ins" data-i18n-label="signinsLabel"></div>
112
124
 
113
125
  <div class="step">
114
- <h2>1 · Agent</h2>
115
- <label for="backend">Backend</label>
126
+ <h2 data-i18n="stepAgent">1 · Agent</h2>
127
+ <label for="backend" data-i18n="backend">Backend</label>
116
128
  <select id="backend"></select>
117
129
  <div class="sub" id="backendHint"></div>
118
- <label for="dir">Working directory (absolute path)</label>
119
- <input id="dir" placeholder="/home/you/projects/app">
120
- <label for="name">First agent name</label>
130
+ <label for="dir" data-i18n="dir">Working directory (optional)</label>
131
+ <input id="dir" placeholder="/home/you/projects/app" data-i18n-placeholder="dirPlaceholder" aria-describedby="dirHint">
132
+ <div class="sub" id="dirHint"></div>
133
+ <label for="name" data-i18n="name">First agent name</label>
121
134
  <input id="name" value="agent-1">
122
135
  </div>
123
136
 
124
137
  <div class="step">
125
- <h2>2 · Platform</h2>
138
+ <h2 data-i18n="stepPlatform">2 · Platform</h2>
126
139
  <div class="row">
127
140
  <button id="pickTelegram" class="primary">Telegram</button>
128
141
  <button id="pickDiscord">Discord</button>
129
- <button id="pickWeb">Web only (no bot)</button>
142
+ <button id="pickWeb" data-i18n="webOnly">Web only (no bot)</button>
130
143
  </div>
131
144
  <div class="sub" id="platformHint">Create a bot with @BotFather, then add it to a group.</div>
132
145
  </div>
133
146
 
134
147
  <div class="step" id="credStep">
135
- <h2>3 · Credentials</h2>
136
- <label for="token">Bot token</label>
148
+ <h2 data-i18n="stepCredentials">3 · Credentials</h2>
149
+ <label for="token" data-i18n="token">Bot token</label>
137
150
  <input id="token" type="password" placeholder="123456:ABC-DEF…">
138
- <div class="sub">Written to ~/.agend/.env and never shown again.</div>
139
- <label for="tokenEnv">Environment variable</label>
151
+ <div class="sub" data-i18n="tokenNote">Written to ~/.agend/.env and never shown again.</div>
152
+ <label for="tokenEnv" data-i18n="tokenEnv">Environment variable</label>
140
153
  <input id="tokenEnv" value="AGEND_BOT_TOKEN">
141
- <div class="row"><button id="verify">Verify</button><span class="msg" id="verifyMsg"></span></div>
154
+ <div class="row"><button id="verify" data-i18n="verify">Verify</button><span class="msg" id="verifyMsg"></span></div>
142
155
  <div id="platformFields"></div>
143
156
  </div>
144
157
 
145
158
  <div class="step">
146
- <h2>4 · Review and start</h2>
147
- <div class="row"><button id="preview">Show what will be written</button></div>
159
+ <h2 data-i18n="stepReview">4 · Review and start</h2>
160
+ <div class="row"><button id="preview" data-i18n="preview">Show what will be written</button></div>
148
161
  <pre id="planOut" hidden></pre>
149
162
  <div class="row"><button id="finish" class="primary" disabled>Create and start AgEnD</button><span class="msg" id="finishMsg"></span></div>
150
163
  </div>
@@ -152,8 +165,9 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
152
165
  <script>
153
166
  (() => {
154
167
  "use strict";
168
+ ${setupI18nRuntime(SETUP_FORM_STRINGS)}
155
169
  const $ = (id) => document.getElementById(id);
156
- const state = { platform: "telegram", identity: null, guilds: [], offset: 0, plan: null };
170
+ const state = { platform: "telegram", identity: null, guilds: [], offset: 0, plan: null, workspaces: null };
157
171
  // Relative to the page's own directory, which is /s/<sid>/. An absolute path
158
172
  // would leave that namespace and 404 — the sid is where this page lives, not
159
173
  // a prefix bolted on to it.
@@ -162,31 +176,37 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
162
176
  let body = null; try { body = await res.json(); } catch {}
163
177
  return { ok: res.ok, status: res.status, body };
164
178
  };
179
+ // A line of this page in its language: a key and its values, or (host) the host's own words, translated when known.
180
+ const say = (id, key, ...values) => show($(id), el => { el.textContent = tr(key, ...values); });
181
+ const sayHost = (id, text, fallback) => show($(id), el => { el.textContent = text ? trHost(text) : tr(fallback); });
182
+
183
+ // #2726: no directory is the agent's own workspace, <AGEND_HOME>/workspaces/<name> — said under the field, by name.
184
+ const showDirHint = () => show($("dirHint"), el => {
185
+ const name = $("name").value.trim();
186
+ el.textContent = state.workspaces && name ? tr("dirDefault", state.workspaces + "/" + name) : tr("dirDefaultName");
187
+ });
165
188
 
166
189
  async function loadEnvironment() {
167
190
  const res = await api("api/settings/quickstart/environment");
168
191
  if (formFrozen) return;
169
192
  const backends = res.body?.backends || [];
193
+ if (typeof res.body?.workspaces_dir === "string") { state.workspaces = res.body.workspaces_dir; showDirHint(); }
170
194
  $("backend").innerHTML = "";
171
195
  for (const name of backends.length ? backends : ["claude-code"]) {
172
196
  const opt = document.createElement("option"); opt.value = name; opt.textContent = name; $("backend").append(opt);
173
197
  }
174
- $("backendHint").textContent = backends.length
175
- ? "Found on this host: " + backends.join(", ")
176
- : "No backend CLI found on this host — install one first.";
198
+ if (backends.length) say("backendHint", "backendFound", backends.join(", "));
199
+ else say("backendHint", "backendNone");
177
200
  }
178
201
 
179
202
  function setPlatform(platform) {
180
203
  if (formFrozen) return;
181
204
  state.platform = platform;
182
- state.identity = null; $("verifyMsg").textContent = "";
205
+ state.identity = null; say("verifyMsg", "");
183
206
  $("pickTelegram").className = platform === "telegram" ? "primary" : "";
184
207
  $("pickDiscord").className = platform === "discord" ? "primary" : "";
185
208
  $("pickWeb").className = platform === "web" ? "primary" : "";
186
- $("platformHint").textContent = platform === "telegram"
187
- ? "Create a bot with @BotFather, then add it to a group."
188
- : platform === "discord" ? "Create an application in the Discord developer portal and invite the bot to your server."
189
- : "No bot: you use AgEnD from its web dashboard (chat, sign-ins, settings). Discord or Telegram can be added later in Settings.";
209
+ say("platformHint", platform === "telegram" ? "hintTelegram" : platform === "discord" ? "hintDiscord" : "hintWeb");
190
210
  // #2233: web only has no credentials step — no token, no admin id, nothing to verify.
191
211
  $("credStep").hidden = platform === "web";
192
212
  state.plan = null; $("planOut").hidden = true; $("finish").disabled = true;
@@ -198,47 +218,48 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
198
218
  if (formFrozen) return;
199
219
  const host = $("platformFields"); host.innerHTML = "";
200
220
  if (state.platform === "web") return;
201
- const field = (id, labelText, hint) => {
202
- const label = document.createElement("label"); label.textContent = labelText; label.htmlFor = id;
221
+ const field = (id, labelKey, hintKey) => {
222
+ const label = document.createElement("label"); label.htmlFor = id; show(label, el => { el.textContent = tr(labelKey); });
203
223
  const input = document.createElement("input"); input.id = id;
204
224
  host.append(label, input);
205
- if (hint) { const h = document.createElement("div"); h.className = "sub"; h.textContent = hint; host.append(h); }
225
+ if (hintKey) { const h = document.createElement("div"); h.className = "sub"; show(h, el => { el.textContent = tr(hintKey); }); host.append(h); }
206
226
  return input;
207
227
  };
208
228
  if (state.platform === "discord") {
209
229
  const guild = document.createElement("select"); guild.id = "guild";
210
- const label = document.createElement("label"); label.textContent = "Server (guild)"; label.htmlFor = "guild";
230
+ const label = document.createElement("label"); label.htmlFor = "guild"; show(label, el => { el.textContent = tr("guild"); });
211
231
  host.append(label, guild);
212
232
  for (const g of state.guilds) { const opt = document.createElement("option"); opt.value = g.id; opt.textContent = g.name + " (" + g.id + ")"; guild.append(opt); }
213
- field("general", "General channel id");
214
- field("admin", "Your Discord user id", "Becomes the first allowed user.");
233
+ field("general", "general");
234
+ field("admin", "discordAdmin", "firstAllowed");
215
235
  } else {
216
- field("group", "Group id", "Or post any message in the group and press Detect.");
236
+ field("group", "group", "groupHint");
217
237
  const row = document.createElement("div"); row.className = "row";
218
- const detect = document.createElement("button"); detect.id = "detect"; detect.textContent = "Detect from a message";
238
+ const detect = document.createElement("button"); detect.id = "detect"; show(detect, el => { el.textContent = tr("detect"); });
219
239
  detect.onclick = detectGroup; row.append(detect);
220
240
  const msg = document.createElement("span"); msg.className = "msg"; msg.id = "detectMsg"; row.append(msg);
221
241
  host.append(row);
222
- field("admin", "Your Telegram user id", "Becomes the first allowed user.");
242
+ field("admin", "telegramAdmin", "firstAllowed");
223
243
  }
224
244
  }
225
245
 
226
246
  async function detectGroup() {
227
247
  if (formFrozen) return;
228
- $("detectMsg").textContent = "Waiting for a message in the group…";
248
+ say("detectMsg", "detectWaiting");
229
249
  const res = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "await-telegram-start", token: $("token").value.trim(), offset: state.offset }) });
230
250
  if (formFrozen) return;
231
- if (!res.ok) { $("detectMsg").textContent = res.body?.error || "Failed."; return; }
251
+ if (!res.ok) { sayHost("detectMsg", res.body?.error, "failed"); return; }
232
252
  state.offset = res.body.offset;
233
253
  if (res.body.found) {
234
254
  $("group").value = String(res.body.found.groupId);
235
255
  $("admin").value = String(res.body.found.userId);
236
- $("detectMsg").textContent = "Found.";
237
- } else $("detectMsg").textContent = "No group message yet — post one and press Detect again.";
256
+ say("detectMsg", "detectFound");
257
+ } else say("detectMsg", "detectNone");
238
258
  }
239
259
 
240
260
  function input() {
241
261
  // #2233: the agent alone; a web-only setup carries no connection field at all.
262
+ // #2726: an empty directory is sent as it is — the host gives the agent its own workspace.
242
263
  if (state.platform === "web") return { web_only: true, backend: $("backend").value, working_directory: $("dir").value.trim(), instance_name: $("name").value.trim() };
243
264
  return {
244
265
  platform: state.platform,
@@ -255,14 +276,16 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
255
276
 
256
277
  $("verify").onclick = async () => {
257
278
  if (formFrozen) return;
258
- $("verifyMsg").textContent = "Asking the provider…";
279
+ say("verifyMsg", "verifying");
259
280
  const token = $("token").value.trim();
260
281
  const res = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform: state.platform, token }) });
261
282
  if (formFrozen) return;
262
- state.identity = res.body?.identity || { valid: false };
263
- $("verifyMsg").className = "msg " + (state.identity.valid ? "ok" : "err");
264
- $("verifyMsg").textContent = state.identity.valid ? "✓ " + (state.identity.username || "verified") : "✗ " + (state.identity.reason || "rejected");
265
- if (state.identity.valid && state.platform === "discord") {
283
+ const identity = state.identity = res.body?.identity || { valid: false };
284
+ show($("verifyMsg"), el => {
285
+ el.className = "msg " + (identity.valid ? "ok" : "err");
286
+ el.textContent = identity.valid ? "✓ " + (identity.username || tr("verified")) : "✗ " + (identity.reason ? trHost(identity.reason) : tr("rejected"));
287
+ });
288
+ if (identity.valid && state.platform === "discord") {
266
289
  const guilds = await api("api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token }) });
267
290
  if (formFrozen) return;
268
291
  state.guilds = guilds.body?.guilds || [];
@@ -274,10 +297,10 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
274
297
  if (formFrozen) return;
275
298
  const res = await api("api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(input()) });
276
299
  if (formFrozen) return;
277
- if (!res.ok) { $("planOut").hidden = false; $("planOut").textContent = res.body?.error || "Failed."; return; }
300
+ if (!res.ok) { $("planOut").hidden = false; sayHost("planOut", res.body?.error, "failed"); return; }
278
301
  state.plan = res.body;
279
302
  $("planOut").hidden = false;
280
- $("planOut").textContent = JSON.stringify(res.body, null, 2);
303
+ show($("planOut"), el => { el.textContent = JSON.stringify(res.body, null, 2); });
281
304
  $("finish").disabled = !(state.platform === "web" || state.identity?.valid);
282
305
  };
283
306
 
@@ -287,13 +310,16 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
287
310
  let pollGeneration = 0;
288
311
  // #2650 item 10: how long this page has left, counted here from the host's own remaining time (no polling: a request
289
312
  // would keep an idle page open). After it, the page says it has closed and how to reopen it, instead of freezing.
290
- const REOPEN = "Reopen it where you started setup: in the Windows installer, press Enter in its window; otherwise run agend setup again on the computer running AgEnD.";
291
313
  let closesAt = null, handedOver = false, pageClosed = false;
314
+ const setFinishLabel = (key) => show($("finish"), el => { el.textContent = tr(key); });
315
+ // finishMsg: a key of this page, or the host's words; with its colour.
316
+ const finishSay = (cls, key, ...values) => show($("finishMsg"), el => { el.className = cls; el.textContent = tr(key, ...values); });
317
+ const finishHost = (text, fallback) => show($("finishMsg"), el => { el.className = "msg err"; el.textContent = text ? trHost(text) : tr(fallback); });
292
318
  const closePage = () => {
293
319
  if (handedOver || pageClosed) return;
294
320
  pageClosed = true; pollGeneration++; freezeFields(true); $("finish").disabled = true;
295
- $("ttlMsg").className = "sub msg err"; $("ttlMsg").textContent = "This setup page has closed.";
296
- $("finishMsg").className = "msg err"; $("finishMsg").textContent = "This setup page has closed. " + REOPEN + " Backends you already signed in stay signed in.";
321
+ show($("ttlMsg"), el => { el.className = "sub msg err"; el.textContent = tr("closed"); });
322
+ show($("finishMsg"), el => { el.className = "msg err"; el.textContent = tr("closedFinish", tr("reopen")); });
297
323
  };
298
324
  const showRemaining = () => {
299
325
  if (closesAt === null || handedOver || pageClosed) return;
@@ -301,9 +327,10 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
301
327
  if (left <= 0) { closePage(); return; }
302
328
  const total = Math.ceil(left / 1000), text = Math.floor(total / 60) + ":" + String(total % 60).padStart(2, "0");
303
329
  const soon = left <= 3 * 60_000;
304
- $("ttlMsg").className = soon ? "sub msg err" : "sub msg";
305
- $("ttlMsg").textContent = "This page closes in " + text + " at the latest (sooner after 10 minutes without activity)."
306
- + (soon ? " Finish soon. If it closes first: " + REOPEN : "");
330
+ show($("ttlMsg"), el => {
331
+ el.className = soon ? "sub msg err" : "sub msg";
332
+ el.textContent = soon ? tr("closesSoon", tr("closesIn", text), tr("reopen")) : tr("closesIn", text);
333
+ });
307
334
  setTimeout(showRemaining, 1000);
308
335
  };
309
336
  const trackRemaining = (status) => {
@@ -316,39 +343,45 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
316
343
  formFrozen = frozen;
317
344
  for (const field of document.querySelectorAll("main input, main select, #verify, #preview, #pickTelegram, #pickDiscord, #pickWeb, #detect")) field.disabled = frozen;
318
345
  };
319
- const restoreEditing = (message) => {
346
+ const restoreEditing = (render) => {
320
347
  if (pageClosed) return; // a late answer never reopens a closed page (#2653 review)
321
348
  pollGeneration++; approvedSetup = false; submission = null; state.identity = null;
322
- freezeFields(false); $("finish").disabled = true; $("finish").textContent = "Create and start AgEnD";
323
- $("finishMsg").className = "msg err"; $("finishMsg").textContent = message;
349
+ freezeFields(false); $("finish").disabled = true; setFinishLabel("finish");
350
+ render();
324
351
  };
325
352
  for (const field of document.querySelectorAll("main input, main select")) field.addEventListener("input", () => {
326
- approvedSetup = false; submission = null; $("finish").textContent = "Create and start AgEnD";
353
+ approvedSetup = false; submission = null; setFinishLabel("finish");
327
354
  });
355
+ // A host confirmation that did not apply: its outcome in this page's language (by its reason), else the host's words.
356
+ const outcomeText = (pending) => {
357
+ const reason = pending.outcome?.reason_code;
358
+ if (reason && I18N.en["outcome_" + reason]) return tr("outcome_" + reason);
359
+ return pending.outcome?.message ? trHost(pending.outcome.message) : tr("notApplied");
360
+ };
328
361
  const watchPending = (pendingId) => {
329
362
  if (pageClosed) return; // a late pending list or commit answer: the page stays closed
330
363
  const generation = ++pollGeneration;
331
364
  freezeFields(true); $("finish").disabled = true;
332
365
  // #2650 item 1: the page asks; a person at the computer running AgEnD approves. Nothing on this page can.
333
- const ask = "Approve this on the computer running AgEnD. The Windows installer opens a confirmation window for it, and shows a command to paste if no window appears. Otherwise run there: agend settings confirm " + pendingId;
334
- $("finishMsg").className = "msg"; $("finishMsg").textContent = ask;
366
+ const ask = () => finishSay("msg", "approveHost", pendingId);
367
+ ask();
335
368
  let misses = 0;
336
369
  const poll = async () => {
337
370
  if (generation !== pollGeneration) return;
338
371
  try {
339
372
  const result = await api("api/settings/pending/" + pendingId);
340
373
  if (generation !== pollGeneration) return;
341
- if (misses) { misses = 0; $("finishMsg").className = "msg"; $("finishMsg").textContent = ask; }
342
- if (!result.ok) { restoreEditing("Setup confirmation is no longer available. Review and enter the token again."); return; }
374
+ if (misses) { misses = 0; ask(); }
375
+ if (!result.ok) { restoreEditing(() => finishSay("msg err", "confirmGone")); return; }
343
376
  const pending = result.body;
344
377
  if (pending.state === "pending" || pending.state === "applying") { setTimeout(poll, 1000); return; }
345
- if (pending.state !== "applied") { restoreEditing(pending.outcome?.message || "Change was not applied. Review and enter the token again."); return; }
346
- approvedSetup = true; submission = null; $("finish").disabled = false; $("finish").textContent = "Start AgEnD";
347
- $("finishMsg").textContent = "Configuration confirmed. Select Start AgEnD to finish setup.";
378
+ if (pending.state !== "applied") { restoreEditing(() => show($("finishMsg"), el => { el.className = "msg err"; el.textContent = outcomeText(pending); })); return; }
379
+ approvedSetup = true; submission = null; $("finish").disabled = false; setFinishLabel("start");
380
+ finishSay("msg", "confirmed");
348
381
  } catch {
349
382
  if (generation !== pollGeneration) return;
350
383
  // #2650 item 10: a lost connection is said, not waited out silently; the pending confirmation keeps its id.
351
- if (++misses === 5) { $("finishMsg").className = "msg err"; $("finishMsg").textContent = "Lost contact with the setup page; still trying. If this stays, the page has closed. " + REOPEN; }
384
+ if (++misses === 5) show($("finishMsg"), el => { el.className = "msg err"; el.textContent = tr("lostContact", tr("reopen")); });
352
385
  setTimeout(poll, misses < 5 ? 1000 : 3000);
353
386
  }
354
387
  };
@@ -357,15 +390,15 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
357
390
  $("finish").onclick = async () => {
358
391
  if (pageClosed) return;
359
392
  if ($("setupSignins").dataset.ready === "false") {
360
- $("finishMsg").textContent = "Finish backend sign-in or choose Sign in later before continuing."; return;
393
+ finishSay($("finishMsg").className, "finishSignins"); return;
361
394
  }
362
395
  $("finish").disabled = true;
363
396
  try {
364
397
  if (approvedSetup) {
365
398
  const finished = await api("setup/finish", { method: "POST" });
366
- if (!finished.ok) { restoreEditing(finished.body?.error || "Confirmation is no longer current."); return; }
367
- handedOver = true; $("ttlMsg").textContent = "";
368
- $("finishMsg").textContent = state.platform === "web" ? "AgEnD is starting. Its dashboard link shows here once it is up." : "AgEnD is starting. Continue in the channel you configured.";
399
+ if (!finished.ok) { restoreEditing(() => finishHost(finished.body?.error, "notCurrent")); return; }
400
+ handedOver = true; show($("ttlMsg"), el => { el.textContent = ""; });
401
+ finishSay($("finishMsg").className, state.platform === "web" ? "startingWeb" : "startingChat");
369
402
  if (finished.body?.watch !== false) waitForFleet();
370
403
  return;
371
404
  }
@@ -373,13 +406,13 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
373
406
  // a terminal outcome receives a new key and requires secret reentry.
374
407
  submission ||= { key: crypto.randomUUID(), body: JSON.stringify(state.platform === "web" ? input() : Object.assign({}, input(), { token: $("token").value.trim() })) };
375
408
  freezeFields(true);
376
- $("finishMsg").textContent = "Requesting host confirmation…";
409
+ finishSay($("finishMsg").className, "requesting");
377
410
  const commit = await api("api/settings/quickstart/commit", { method: "POST", headers: { "Content-Type": "application/json", "Idempotency-Key": submission.key }, body: submission.body });
378
- if (!commit.ok) { restoreEditing(commit.body?.error || "Failed."); return; }
379
- if (commit.body?.result !== "pending_confirmation" || !commit.body.pending_change?.id) { restoreEditing("Expected a pending host confirmation."); return; }
411
+ if (!commit.ok) { restoreEditing(() => finishHost(commit.body?.error, "failed")); return; }
412
+ if (commit.body?.result !== "pending_confirmation" || !commit.body.pending_change?.id) { restoreEditing(() => finishSay("msg err", "expectedPending")); return; }
380
413
  $("token").value = ""; submission = null;
381
414
  watchPending(commit.body.pending_change.id);
382
- } catch { if (pageClosed) return; $("finishMsg").textContent = "Connection failed. Retry to find the same confirmation."; $("finish").disabled = false; }
415
+ } catch { if (pageClosed) return; finishSay($("finishMsg").className, "connectionFailed"); $("finish").disabled = false; }
383
416
  };
384
417
  // Same-cookie reloads rediscover an unresolved consent request without a secret.
385
418
  const status = api("setup/status").then(result => { if (result.ok) trackRemaining(result.body); return result; });
@@ -389,8 +422,8 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
389
422
  if (pending) { watchPending(pending.id); return; }
390
423
  status.then(current => {
391
424
  if (pageClosed || !current.ok || !current.body?.finish_ready || submission || pollGeneration) return;
392
- approvedSetup = true; freezeFields(true); $("finish").disabled = false; $("finish").textContent = "Start AgEnD";
393
- $("finishMsg").textContent = "Configuration confirmed. Select Start AgEnD to finish setup.";
425
+ approvedSetup = true; freezeFields(true); $("finish").disabled = false; setFinishLabel("start");
426
+ finishSay($("finishMsg").className, "confirmed");
394
427
  }).catch(() => {});
395
428
  }).catch(() => {});
396
429
 
@@ -402,23 +435,19 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
402
435
  try {
403
436
  const res = await fetch("/health", { cache: "no-store" });
404
437
  if (res.status) {
405
- $("finishMsg").className = "msg ok";
406
438
  // #1519 P7: the fleet answers on this address now — its dashboard is here too.
407
- const link = document.createElement("a"); link.href = "/"; link.textContent = location.origin + "/";
408
- if (state.platform === "web") {
409
- // #2233: no bot — the dashboard is where AgEnD is used; its sign-in code comes from the machine.
410
- $("finishMsg").textContent = "AgEnD is up. Open its dashboard: ";
411
- $("finishMsg").append(link, " — sign in with the code from agend web --code on the machine running AgEnD. Sign your agents' CLIs in from Settings → Agents → Sign-ins.");
412
- return;
413
- }
414
- $("finishMsg").textContent = "AgEnD is up. Talk to it in the channel you just set up, or open its dashboard: ";
415
- $("finishMsg").append(link, " (sign in with agend web --code on the machine running AgEnD, or send /web to your bot).");
439
+ // #2233: with no bot, the dashboard is where AgEnD is used; its sign-in code comes from the machine.
440
+ show($("finishMsg"), el => {
441
+ el.className = "msg ok";
442
+ const link = document.createElement("a"); link.href = "/"; link.textContent = location.origin + "/";
443
+ const [before, after] = tr(state.platform === "web" ? "upWeb" : "upChat").split("{0}");
444
+ el.textContent = before; el.append(link, after ?? "");
445
+ });
416
446
  return;
417
447
  }
418
448
  } catch { /* not listening yet */ }
419
449
  if (Date.now() > deadline) {
420
- $("finishMsg").className = "msg err";
421
- $("finishMsg").textContent = "AgEnD has not come up within 60 seconds. Run \`agend start\` on the machine itself and check fleet.log — this page cannot tell you any more.";
450
+ finishSay("msg err", "notUp");
422
451
  return;
423
452
  }
424
453
  setTimeout(tick, 1000);
@@ -429,6 +458,9 @@ export const SETUP_FORM_HTML = `<!DOCTYPE html>
429
458
  $("pickTelegram").onclick = () => setPlatform("telegram");
430
459
  $("pickDiscord").onclick = () => setPlatform("discord");
431
460
  $("pickWeb").onclick = () => setPlatform("web");
461
+ $("name").addEventListener("input", showDirHint);
462
+ setFinishLabel("finish");
463
+ showDirHint();
432
464
  loadEnvironment();
433
465
  setPlatform("telegram");
434
466
  })();
@@ -1 +1 @@
1
- {"version":3,"file":"setup-form.js","sourceRoot":"","sources":["../src/setup-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8DnC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuW9B,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,OAAO;QACL,oBAAoB;QACpB,qBAAqB,KAAK,GAAG;QAC7B,oBAAoB,KAAK,GAAG;QAC5B,sBAAsB;QACtB,oBAAoB;QACpB,iBAAiB;QACjB,oBAAoB;QACpB,wBAAwB;KACzB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACnD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,kBAAkB,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,iBAAiB,KAAK,IAAI,CAAC,CAAC;AACvH,CAAC"}
1
+ {"version":3,"file":"setup-form.js","sourceRoot":"","sources":["../src/setup-form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE3F;;;;;;;;GAQG;AACH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAqChC,gBAAgB,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCvC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8E3B,gBAAgB,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+SvC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,OAAO;QACL,oBAAoB;QACpB,qBAAqB,KAAK,GAAG;QAC7B,oBAAoB,KAAK,GAAG;QAC5B,sBAAsB;QACtB,oBAAoB;QACpB,iBAAiB;QACjB,oBAAoB;QACpB,wBAAwB;KACzB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,+GAA+G;AAC/G,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACnD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,kBAAkB,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,iBAAiB,KAAK,IAAI,CAAC,CAAC;AACvH,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * #2726: the setup pages in English and 繁體中文.
3
+ *
4
+ * Both pages are inline (setup-form.ts) and run before any fleet — no dashboard, no /assets — so they carry their own
5
+ * small dictionary and the few lines that use it (SETUP_I18N_RUNTIME). The choice works as the dashboard's does
6
+ * (src/ui/shared/app-i18n.js): the language chosen in this browser (`agend_lang`, the dashboard's own key, so the
7
+ * dashboard opened afterwards on this address keeps it), else the browser's language; a toggle on the page changes it.
8
+ * The wording is the dashboard's where it already says the same thing (settings-strings.js wizard*).
9
+ *
10
+ * What the host says (its errors) is English, or a code: src/ui/shared/setup-host-text.js says each one the page can show
11
+ * in both languages — the same module the dashboard's Setup wizard uses. One it does not know is shown as it came.
12
+ */
13
+ export type SetupLang = "en" | "zh-TW";
14
+ type Table = Record<string, string>;
15
+ /** The code-entry page: before the code, nothing about this machine (setup-form.ts SETUP_CODE_PAGE_HTML). */
16
+ export declare const SETUP_CODE_STRINGS: Record<SetupLang, Table>;
17
+ /** The setup form (setup-form.ts SETUP_FORM_HTML). */
18
+ export declare const SETUP_FORM_STRINGS: Record<SetupLang, Table>;
19
+ /**
20
+ * The few lines both pages run first: the dictionary (JSON, `<` escaped so no string can end the script), the language,
21
+ * `tr` (a key in it), `trHost` (a host message in it), `show` (a text that follows the language when it changes) and
22
+ * `applyLang`. Plain script, no `${` and no template literal: it is pasted into the pages' own template strings.
23
+ */
24
+ export declare function setupI18nRuntime(strings: Record<SetupLang, Table>): string;
25
+ export {};