made-bridge 0.3.4 → 0.3.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "made-bridge",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "码得(made)独立语音编程设备的电脑侧桥接器:工作台 API、设备接口、任务调度与插件运行时。一条命令启动,配合 ESP32 固件使用,源码在 github.com/jobop/made。",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/public/app.js CHANGED
@@ -6,7 +6,7 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
6
6
  const $ = (id) => document.getElementById(id);
7
7
  const taskPage = document.body.dataset.page === 'tasks';
8
8
  const assistantsPage = document.body.dataset.page === 'assistants';
9
- const state = { jobs: [], sessions: [], providers: [], projects: [], connection: {}, voice: {}, models: {}, pairing: { open: false, pending: [], paired: [] }, tunnel: { status: 'off' }, receiver: { status: 'disabled' }, receiverPasswordVisible: false, token: null, lastSyncedAt: null, loaded: false, error: '', busy: new Set(), pairBusy: false, pairWindowBusy: false, tunnelBusy: false, dismissedPairing: new Set(), selectedSessionId: null, previewProvider: '', previewSessionId: null, sessionFormOpen: false, legacySessionsOpen: false, renamingSession: false, refreshing: false };
9
+ const state = { jobs: [], sessions: [], providers: [], projects: [], connection: {}, voice: {}, models: {}, pairing: { open: false, pending: [], paired: [] }, tunnel: { status: 'off' }, receiver: { status: 'disabled' }, receiverPasswordVisible: false, token: null, lastSyncedAt: null, loaded: false, error: '', busy: new Set(), pairBusy: false, pairWindowBusy: false, tunnelBusy: false, dismissedPairing: new Set(), selectedSessionId: null, previewProvider: '', previewSessionId: null, sessionFormOpen: false, legacySessionsOpen: false, renamingSession: false, refreshing: false, historyOpen: false };
10
10
  if (taskPage) {
11
11
  try { state.selectedSessionId = sessionStorage.getItem('vibe-selected-session'); } catch { /* Selection works without storage. */ }
12
12
  const params = new URLSearchParams(window.location.search);
@@ -64,6 +64,9 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
64
64
  const canConfirm = (job) => job.status === 'waiting_confirmation';
65
65
  const canCancel = (job) => ['waiting_confirmation', 'queued', 'running'].includes(job.status);
66
66
  const timeText = (value) => { const date = new Date(value); return Number.isNaN(date.getTime()) ? '' : new Intl.DateTimeFormat(getLocale(), { hour: '2-digit', minute: '2-digit' }).format(date); };
67
+ const turnAnswer = (job) => userFacingJobError(job) || (job.status === 'running' ? job.progress : '') || job.result ||
68
+ (job.status === 'waiting_confirmation' ? t('app.074') :
69
+ job.status === 'queued' ? t('app.075') : t('app.076'));
67
70
  const setFormMessage = (message, success = false) => { setUiMessage($('formMessage'), message); $('formMessage').classList.toggle('success', success); };
68
71
  let toastTimer;
69
72
  function showToast(message, error = false) { const toast = $('toast'); setUiMessage(toast, message); toast.classList.toggle('error', error); toast.classList.add('visible'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('visible'), 4200); }
@@ -314,9 +317,7 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
314
317
  const top = text('div', 'job-top');
315
318
  top.append(text('div', 'job-meta', t('app.071', { v0: (timeText(job.createdAt) || t('app.047')) })), text('span', `job-status ${info.css}`, info.label));
316
319
  card.append(top, text('div', 'job-instruction', t('app.073', { v0: (job.instruction || t('app.072')) })));
317
- const answer = userFacingJobError(job) || (job.status === 'running' ? job.progress : '') || job.result ||
318
- (job.status === 'waiting_confirmation' ? t('app.074') :
319
- job.status === 'queued' ? t('app.075') : t('app.076'));
320
+ const answer = turnAnswer(job);
320
321
  card.append(text('p', `job-summary turn-answer ${job.error && !job.retryPending ? 'error' : ''}`, t('app.077', { v0: (providerName(job.provider)), v1: (safeString(answer)) })));
321
322
  if (job.provider === 'codex' && job.retryPending) {
322
323
  card.append(text('p', 'job-retry-notice', t('app.078')));
@@ -328,6 +329,38 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
328
329
  if (actions.children.length) { const bottom = text('div', 'job-bottom'); bottom.append(actions); card.append(bottom); }
329
330
  list.append(card);
330
331
  }
332
+ /** 历史会话抽屉:当前任务的全部指令与回复,新的在上。数据来自 /api/state 的 jobs。 */
333
+ function renderHistory() {
334
+ const button = $('historyDrawerButton');
335
+ const overlay = $('historyDrawerOverlay');
336
+ if (!button || !overlay) return;
337
+ const session = selectedSession();
338
+ const jobs = session ? jobsForSession(session.id) : [];
339
+ button.hidden = !session || !!state.error;
340
+ button.setAttribute('aria-expanded', String(state.historyOpen));
341
+ overlay.hidden = !state.historyOpen;
342
+ if (!state.historyOpen) return;
343
+ $('historyDrawerMeta').textContent = session ? `${session.title || t('app.045')} — ${t('app.227')}` : t('app.227');
344
+ const list = $('historyList'); list.replaceChildren();
345
+ if (!jobs.length) {
346
+ list.append(text('p', 'history-empty', t('app.229')));
347
+ return;
348
+ }
349
+ [...jobs].reverse().forEach((job, index) => {
350
+ const info = jobStatus(job);
351
+ const turn = text('article', 'history-turn');
352
+ const top = text('div', 'history-turn-top');
353
+ top.append(
354
+ text('span', 'turn-number', t('app.228', { v0: jobs.length - index })),
355
+ text('span', 'history-turn-time', formatDateTime(job.createdAt)),
356
+ text('span', `job-status ${info.css}`, info.label),
357
+ );
358
+ turn.append(top, text('div', 'history-turn-instruction', job.instruction || t('app.072')));
359
+ const answer = turnAnswer(job);
360
+ if (answer) turn.append(text('p', `history-turn-answer ${job.error && !job.retryPending ? 'error' : ''}`, answer));
361
+ list.append(turn);
362
+ });
363
+ }
331
364
  const boardEncoder = new TextEncoder();
332
365
  function boardCleanText(value) {
333
366
  return safeString(value)
@@ -912,7 +945,7 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
912
945
  setUiMessage($('firmwareMessage'), error.message || '');
913
946
  } finally { firmwareBusy = false; }
914
947
  }
915
- function render() { renderSelects(); renderSessions(); renderJobs(); renderBoard(); renderPairing(); renderTunnel(); renderReceiver(); renderFirmware(); renderConnection(); renderModelSettings(); renderVoiceSettings(); renderProviders(); }
948
+ function render() { renderSelects(); renderSessions(); renderJobs(); renderHistory(); renderBoard(); renderPairing(); renderTunnel(); renderReceiver(); renderFirmware(); renderConnection(); renderModelSettings(); renderVoiceSettings(); renderProviders(); }
916
949
  function applyLanguage(locale) {
917
950
  const changed = setLocale(locale);
918
951
  if (!changed && document.documentElement.lang === locale) {
@@ -992,6 +1025,25 @@ import { t, m, getLocale, setLocale, supportedLocales, applyStaticTranslations,
992
1025
  state.legacySessionsOpen = !state.legacySessionsOpen;
993
1026
  renderSessions();
994
1027
  });
1028
+ $('historyDrawerButton')?.addEventListener('click', () => {
1029
+ state.historyOpen = !state.historyOpen;
1030
+ renderHistory();
1031
+ });
1032
+ $('historyDrawerClose')?.addEventListener('click', () => {
1033
+ state.historyOpen = false;
1034
+ renderHistory();
1035
+ });
1036
+ $('historyDrawerOverlay')?.addEventListener('click', (event) => {
1037
+ if (event.target !== event.currentTarget) return;
1038
+ state.historyOpen = false;
1039
+ renderHistory();
1040
+ });
1041
+ document.addEventListener('keydown', (event) => {
1042
+ if (event.key === 'Escape' && state.historyOpen) {
1043
+ state.historyOpen = false;
1044
+ renderHistory();
1045
+ }
1046
+ });
995
1047
  $('sessionForm')?.addEventListener('submit', (event) => { event.preventDefault(); createSession(); });
996
1048
  $('renameSessionButton')?.addEventListener('click', () => {
997
1049
  const session = selectedSession();
@@ -900,6 +900,26 @@ export const messages = {
900
900
  "zh-CN": "创建失败",
901
901
  "en": "Creation failed"
902
902
  },
903
+ "app.226": {
904
+ "zh-CN": "历史记录",
905
+ "en": "History"
906
+ },
907
+ "app.227": {
908
+ "zh-CN": "这个任务的全部指令与回复,新的在上面。",
909
+ "en": "Every instruction and reply of this task, newest first."
910
+ },
911
+ "app.228": {
912
+ "zh-CN": "第 {v0} 轮",
913
+ "en": "Turn {v0}"
914
+ },
915
+ "app.229": {
916
+ "zh-CN": "这个任务还没有对话记录。",
917
+ "en": "No conversation history for this task yet."
918
+ },
919
+ "app.230": {
920
+ "zh-CN": "关闭",
921
+ "en": "Close"
922
+ },
903
923
  "tunnel.official.title": {
904
924
  "zh-CN": "官方入口",
905
925
  "en": "Official links"
package/public/styles.css CHANGED
@@ -443,3 +443,27 @@
443
443
  .tunnel-service-guide { padding: 12px; }
444
444
  .tunnel-official-link { flex: 1 1 auto; }
445
445
  }
446
+
447
+ /* 历史会话抽屉:任务页「最新进展」旁的按钮拉开,展示当前任务的全部指令与回复 */
448
+ .history-overlay { position: fixed; inset: 0; z-index: 15; background: rgba(74, 62, 44, .38); backdrop-filter: blur(4px); }
449
+ .history-overlay[hidden] { display: none; }
450
+ .history-drawer { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; width: min(560px, 100%); height: 100%; background: var(--bg); border-left: 1px solid var(--line); box-shadow: -18px 0 48px rgba(74, 62, 44, .22); animation: history-in .22s ease; }
451
+ @keyframes history-in { from { transform: translateX(28px); opacity: 0; } to { transform: none; opacity: 1; } }
452
+ .history-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 22px 22px 16px; border-bottom: 1px solid var(--line); background: var(--panel); }
453
+ .history-head h2 { margin-top: 6px; font-size: var(--font-heading); font-weight: 700; }
454
+ .history-head .panel-intro { margin-top: 6px; }
455
+ .history-head .icon-button { flex: 0 0 auto; }
456
+ .history-list { flex: 1; display: grid; gap: 10px; align-content: start; padding: 18px 22px 30px; overflow-y: auto; }
457
+ .history-empty { color: var(--dim); font-size: var(--font-small); padding: 13px; }
458
+ .history-turn { border: 1px solid var(--line); border-radius: 15px; background: var(--panel); padding: 14px 15px; }
459
+ .history-turn-top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; color: var(--dim); font-size: var(--font-small); font-weight: 600; }
460
+ .history-turn-time { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
461
+ .history-turn-instruction { margin: 9px 0 0; font-size: var(--font-body); font-weight: 600; line-height: 1.55; white-space: pre-wrap; overflow-wrap: anywhere; }
462
+ .history-turn-answer { margin: 7px 0 0; color: var(--muted); font-size: var(--font-body); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
463
+ .history-turn-answer.error { color: var(--red); }
464
+ @media (max-width: 650px) {
465
+ .history-head { padding: 18px 16px 13px; }
466
+ .history-list { padding: 14px 16px 24px; }
467
+ .history-turn { padding: 13px 14px; }
468
+ .history-turn-top { align-items: flex-start; }
469
+ }
package/public/tasks.html CHANGED
@@ -53,7 +53,7 @@
53
53
  </section>
54
54
 
55
55
  <section class="panel queue" aria-labelledby="queue-title">
56
- <div class="panel-head"><div><h2 id="queue-title"><span data-i18n="static.132">最新进展</span></h2><p class="panel-intro"><span data-i18n="static.133">显示这个任务最新一次指令、执行状态和助手回复。</span></p></div></div>
56
+ <div class="panel-head"><div><h2 id="queue-title"><span data-i18n="static.132">最新进展</span></h2><p class="panel-intro"><span data-i18n="static.133">显示这个任务最新一次指令、执行状态和助手回复。</span></p></div><button id="historyDrawerButton" class="small-button" type="button" aria-expanded="false" aria-controls="historyDrawerOverlay" hidden data-i18n="app.226">历史记录</button></div>
57
57
  <div id="jobList" class="job-list" aria-live="polite"><div class="empty"><div class="empty-icon" aria-hidden="true">⌁</div><strong><span data-i18n="static.122">正在读取任务</span></strong><span><span data-i18n="static.134">正在连接本机桥接服务…</span></span></div></div>
58
58
  </section>
59
59
 
@@ -70,6 +70,15 @@
70
70
  </main>
71
71
  <footer class="footer"><span><span data-i18n="static.082">码得 · 多助手桥接</span></span><span id="lastUpdated"><span data-i18n="static.083">等待首次同步</span></span></footer>
72
72
  </div>
73
+ <div id="historyDrawerOverlay" class="history-overlay" hidden>
74
+ <aside class="history-drawer" role="dialog" aria-modal="true" aria-labelledby="historyDrawerTitle">
75
+ <div class="history-head">
76
+ <div><span class="section-kicker"><span data-i18n="static.125">当前任务</span></span><h2 id="historyDrawerTitle"><span data-i18n="app.226">历史记录</span></h2><p id="historyDrawerMeta" class="panel-intro"></p></div>
77
+ <button id="historyDrawerClose" class="icon-button" type="button" title="关闭" aria-label="关闭" data-i18n-title="app.230" data-i18n-aria-label="app.230">×</button>
78
+ </div>
79
+ <div id="historyList" class="history-list"></div>
80
+ </aside>
81
+ </div>
73
82
  <div id="toast" class="toast" role="status" aria-live="polite"></div>
74
83
  <div id="pairingOverlay" class="pairing-overlay" hidden>
75
84
  <section class="pairing-dialog" role="dialog" aria-modal="true" aria-labelledby="pairingDialogTitle" aria-describedby="pairingDialogDescription">
package/src/jobs.mjs CHANGED
@@ -224,7 +224,9 @@ export class JobStore {
224
224
  projectFor(job) {
225
225
  const configured = this.projects.find((item) => item.id === job.projectId);
226
226
  const dir = this.workspacePath(job.vibeSessionId);
227
- return dir ? { ...configured, path: dir } : configured;
227
+ // 每个会话跑在自己的沙箱目录里,但把选定项目的真实路径一并带给插件
228
+ // (originPath):助手类插件可以只读地回看真正的代码,写入仍留在沙箱。
229
+ return dir ? { ...configured, path: dir, originPath: configured?.path || '' } : configured;
228
230
  }
229
231
 
230
232
  createSession(payload) {
package/src/server.mjs CHANGED
@@ -1147,6 +1147,15 @@ if (isEntry) {
1147
1147
  process.once('SIGINT', shutdown);
1148
1148
  process.once('SIGTERM', shutdown);
1149
1149
  process.once('exit', () => { try { app.tunnel.process?.kill('SIGTERM'); } catch { /* Already exited. */ } });
1150
+ // 无声退出兜底(2026-10-20):桥接器在设备锁屏反复断链场景会无栈消失
1151
+ // (一天三次,2min~25min 不等),未捕获异常/拒绝会静默杀进程。桥接器
1152
+ // 是常驻服务——记录后继续跑,状态机脏了也强过整个掉线。
1153
+ process.on('uncaughtException', (error) => {
1154
+ console.error(`[fatal] uncaughtException(继续运行):${error?.stack || error}`);
1155
+ });
1156
+ process.on('unhandledRejection', (reason) => {
1157
+ console.error(`[fatal] unhandledRejection(继续运行):${reason?.stack || reason}`);
1158
+ });
1150
1159
  app.listen().catch((error) => {
1151
1160
  console.error(error.message);
1152
1161
  process.exitCode = 1;