@circuitwall/jarela 1.3.0 → 1.4.1

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 (102) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  5. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  15. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  22. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js +10 -1
  23. package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js.map +1 -1
  24. package/.next/standalone/.next/server/app/api/v1/dashboard/currency/route.js +10 -5
  25. package/.next/standalone/.next/server/app/api/v1/dashboard/currency/route.js.map +1 -1
  26. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +37 -3
  27. package/.next/standalone/.next/server/app/api/v1/page-capture/route.js.map +1 -1
  28. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js +9 -1
  29. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/probe/route.js.map +1 -1
  30. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +33 -8
  31. package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js.map +1 -1
  32. package/.next/standalone/.next/server/app/page.js +73 -204
  33. package/.next/standalone/.next/server/app/page.js.map +1 -1
  34. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/setup/page.js +1 -1
  37. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  39. package/.next/standalone/.next/server/chunks/1718.js +159 -0
  40. package/.next/standalone/.next/server/chunks/1718.js.map +1 -0
  41. package/.next/standalone/.next/server/chunks/2082.js +6 -3
  42. package/.next/standalone/.next/server/chunks/2082.js.map +1 -1
  43. package/.next/standalone/.next/server/chunks/210.js +28 -0
  44. package/.next/standalone/.next/server/chunks/210.js.map +1 -1
  45. package/.next/standalone/.next/server/chunks/423.js +6 -3
  46. package/.next/standalone/.next/server/chunks/423.js.map +1 -1
  47. package/.next/standalone/.next/server/chunks/4631.js +37 -5
  48. package/.next/standalone/.next/server/chunks/4631.js.map +1 -1
  49. package/.next/standalone/.next/server/chunks/8167.js +255 -204
  50. package/.next/standalone/.next/server/chunks/8167.js.map +1 -1
  51. package/.next/standalone/.next/server/chunks/8866.js +38 -5
  52. package/.next/standalone/.next/server/chunks/8866.js.map +1 -1
  53. package/.next/standalone/.next/server/chunks/9032.js +8 -0
  54. package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
  55. package/.next/standalone/.next/server/chunks/{7883.js → 9557.js} +15 -3
  56. package/.next/standalone/.next/server/chunks/9557.js.map +1 -0
  57. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  58. package/.next/standalone/.next/server/middleware.js +6 -3
  59. package/.next/standalone/.next/server/pages/404.html +2 -2
  60. package/.next/standalone/.next/server/pages/500.html +1 -1
  61. package/.next/standalone/.next/server/proxy.js.map +1 -1
  62. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/.next/standalone/.next/static/chunks/{2351-68d8987bbe17ba2d.js → 2351-1ab119fb3b48f4c9.js} +258 -205
  64. package/.next/standalone/.next/static/chunks/2351-1ab119fb3b48f4c9.js.map +1 -0
  65. package/.next/standalone/.next/static/chunks/{9209-0d46118e502f8bf5.js → 4097-64691f9110cf167c.js} +14 -2
  66. package/.next/standalone/.next/static/chunks/4097-64691f9110cf167c.js.map +1 -0
  67. package/.next/standalone/.next/static/chunks/app/{page-2ab710949b62a638.js → page-145150e0468544e7.js} +74 -205
  68. package/.next/standalone/.next/static/chunks/app/page-145150e0468544e7.js.map +1 -0
  69. package/.next/standalone/.next/static/chunks/app/setup/{page-9a465b5fa755b3c3.js → page-a1463a9ace439ff7.js} +2 -2
  70. package/.next/standalone/.next/static/chunks/app/setup/{page-9a465b5fa755b3c3.js.map → page-a1463a9ace439ff7.js.map} +1 -1
  71. package/.next/standalone/.next/static/chunks/{webpack-ff5627013a5e3842.js → webpack-f4ac5c5f92cfd1c1.js} +13 -1
  72. package/.next/standalone/.next/static/chunks/webpack-f4ac5c5f92cfd1c1.js.map +1 -0
  73. package/.next/standalone/package.json +2 -1
  74. package/CHANGELOG.md +84 -0
  75. package/README.md +51 -26
  76. package/api/client.ts +10 -9
  77. package/app/api/v1/dashboard/currency/route.ts +7 -2
  78. package/app/api/v1/providers/[provider]/probe/route.ts +12 -1
  79. package/app/api/v1/threads/[thread_id]/run/route.ts +22 -8
  80. package/components/chat/InputBar.tsx +10 -1
  81. package/components/layout/AppShell.tsx +53 -17
  82. package/components/setup/PinKeypad.tsx +238 -0
  83. package/components/setup/ScreenLock.tsx +8 -173
  84. package/components/setup/UnlockScreen.tsx +25 -192
  85. package/lib/api/page-capture.test.ts +58 -0
  86. package/lib/api/page-capture.ts +31 -1
  87. package/lib/documents/remote/github.ts +16 -2
  88. package/lib/documents/remote/mail.ts +11 -2
  89. package/lib/lifecycle/shutdown.ts +9 -0
  90. package/lib/providers/github-copilot-auth.ts +2 -0
  91. package/lib/providers/github-copilot.ts +1 -0
  92. package/lib/tools/async-results.ts +11 -0
  93. package/package.json +2 -1
  94. package/scripts/install-to-system.ps1 +2 -2
  95. package/scripts/installed-launcher.ps1 +81 -17
  96. package/.next/standalone/.next/server/chunks/7883.js.map +0 -1
  97. package/.next/standalone/.next/static/chunks/2351-68d8987bbe17ba2d.js.map +0 -1
  98. package/.next/standalone/.next/static/chunks/9209-0d46118e502f8bf5.js.map +0 -1
  99. package/.next/standalone/.next/static/chunks/app/page-2ab710949b62a638.js.map +0 -1
  100. package/.next/standalone/.next/static/chunks/webpack-ff5627013a5e3842.js.map +0 -1
  101. /package/.next/standalone/.next/static/{ZKy7LJ3KXj2TIyKOg_fBH → WQdcnm9NyqpeNc0Z8_woo}/_buildManifest.js +0 -0
  102. /package/.next/standalone/.next/static/{ZKy7LJ3KXj2TIyKOg_fBH → WQdcnm9NyqpeNc0Z8_woo}/_ssgManifest.js +0 -0
@@ -13,9 +13,9 @@ Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_re
13
13
  },
14
14
  /******/ __webpack_require__ => { // webpackRuntimeModules
15
15
  /******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
16
- /******/ __webpack_require__.O(0, [9209,2351,8441,3457,7358], () => (__webpack_exec__(7339)));
16
+ /******/ __webpack_require__.O(0, [4097,2351,8441,3457,7358], () => (__webpack_exec__(7339)));
17
17
  /******/ var __webpack_exports__ = __webpack_require__.O();
18
18
  /******/ _N_E = __webpack_exports__;
19
19
  /******/ }
20
20
  ]);
21
- //# sourceMappingURL=page-9a465b5fa755b3c3.js.map
21
+ //# sourceMappingURL=page-a1463a9ace439ff7.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"static/chunks/app/setup/page-9a465b5fa755b3c3.js","mappings":";;;;;AAAA,+FAAgJ;AAChJ;AACA,+FAAwI","sources":["webpack://_N_E/?e6bd"],"sourcesContent":["import(/* webpackMode: \"eager\", webpackExports: [\"OnboardingWizard\"] */ \"/home/runner/work/jarela/jarela/components/setup/OnboardingWizard.tsx\");\n;\nimport(/* webpackMode: \"eager\", webpackExports: [\"UnlockScreen\"] */ \"/home/runner/work/jarela/jarela/components/setup/UnlockScreen.tsx\");\n"],"names":[],"sourceRoot":"","ignoreList":[]}
1
+ {"version":3,"file":"static/chunks/app/setup/page-a1463a9ace439ff7.js","mappings":";;;;;AAAA,+FAAgJ;AAChJ;AACA,+FAAwI","sources":["webpack://_N_E/?e6bd"],"sourcesContent":["import(/* webpackMode: \"eager\", webpackExports: [\"OnboardingWizard\"] */ \"/home/runner/work/jarela/jarela/components/setup/OnboardingWizard.tsx\");\n;\nimport(/* webpackMode: \"eager\", webpackExports: [\"UnlockScreen\"] */ \"/home/runner/work/jarela/jarela/components/setup/UnlockScreen.tsx\");\n"],"names":[],"sourceRoot":"","ignoreList":[]}
@@ -68,6 +68,18 @@
68
68
  /******/ };
69
69
  /******/ })();
70
70
  /******/
71
+ /******/ /* webpack/runtime/compat get default export */
72
+ /******/ (() => {
73
+ /******/ // getDefaultExport function for compatibility with non-harmony modules
74
+ /******/ __webpack_require__.n = (module) => {
75
+ /******/ var getter = module && module.__esModule ?
76
+ /******/ () => (module['default']) :
77
+ /******/ () => (module);
78
+ /******/ __webpack_require__.d(getter, { a: getter });
79
+ /******/ return getter;
80
+ /******/ };
81
+ /******/ })();
82
+ /******/
71
83
  /******/ /* webpack/runtime/create fake namespace object */
72
84
  /******/ (() => {
73
85
  /******/ var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
@@ -327,4 +339,4 @@
327
339
  /******/
328
340
  /******/ })()
329
341
  ;
330
- //# sourceMappingURL=webpack-ff5627013a5e3842.js.map
342
+ //# sourceMappingURL=webpack-f4ac5c5f92cfd1c1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"static/chunks/webpack-f4ac5c5f92cfd1c1.js","mappings":";;;;UAAA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA,GAAG;UACH;UACA;;UAEA;UACA;UACA;;UAEA;UACA;;;;;WC/BA;WACA;WACA;WACA;WACA,+BAA+B,wCAAwC;WACvE;WACA;WACA;WACA;WACA,iBAAiB,qBAAqB;WACtC;WACA;WACA,kBAAkB,qBAAqB;WACvC;WACA;WACA,KAAK;WACL;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;WC3BA;WACA;WACA;WACA;WACA;WACA,iCAAiC,WAAW;WAC5C;WACA;;;;;WCPA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,sDAAsD;WACtD,sCAAsC,iEAAiE;WACvG;WACA;WACA;WACA;WACA;WACA;;;;;WCzBA;WACA;WACA;WACA;WACA,yCAAyC,wCAAwC;WACjF;WACA;WACA;;;;;WCPA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,EAAE;WACF;;;;;WCRA;WACA;WACA;WACA;WACA;;;;;WCJA;WACA;WACA;WACA;WACA;;;;;WCJA;;;;;WCAA;WACA;WACA;WACA;WACA,uBAAuB,4BAA4B;WACnD;WACA;WACA;WACA,iBAAiB,oBAAoB;WACrC;WACA,mGAAmG,YAAY;WAC/G;WACA;WACA;WACA;WACA;;WAEA;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,mEAAmE,iCAAiC;WACpG;WACA;WACA;WACA;;;;;WCzCA;WACA;WACA;WACA,uDAAuD,iBAAiB;WACxE;WACA,gDAAgD,aAAa;WAC7D;;;;;WCNA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;;;;WCZA;;;;;WCAA;;;;;WCAA;;WAEA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA;WACA;WACA,iCAAiC;;WAEjC;WACA;WACA;WACA,KAAK;WACL;WACA;WACA;WACA;;WAEA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,MAAM;WACN;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA;WACA,MAAM,qBAAqB;WAC3B;WACA;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA;WACA","sources":["webpack://_N_E/webpack/bootstrap","webpack://_N_E/webpack/runtime/chunk loaded","webpack://_N_E/webpack/runtime/compat get default export","webpack://_N_E/webpack/runtime/create fake namespace object","webpack://_N_E/webpack/runtime/define property getters","webpack://_N_E/webpack/runtime/ensure chunk","webpack://_N_E/webpack/runtime/get javascript chunk filename","webpack://_N_E/webpack/runtime/get mini-css chunk filename","webpack://_N_E/webpack/runtime/hasOwnProperty shorthand","webpack://_N_E/webpack/runtime/load script","webpack://_N_E/webpack/runtime/make namespace object","webpack://_N_E/webpack/runtime/trusted types policy","webpack://_N_E/webpack/runtime/trusted types script url","webpack://_N_E/webpack/runtime/publicPath","webpack://_N_E/webpack/runtime/jsonp chunk loading","webpack://_N_E/webpack/before-startup","webpack://_N_E/webpack/startup","webpack://_N_E/webpack/after-startup"],"sourcesContent":["// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\tvar threw = true;\n\ttry {\n\t\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\t\tthrew = false;\n\t} finally {\n\t\tif(threw) delete __webpack_module_cache__[moduleId];\n\t}\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","var deferred = [];\n__webpack_require__.O = (result, chunkIds, fn, priority) => {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar [chunkIds, fn, priority] = deferred[i];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => (__webpack_require__.O[key](chunkIds[j])))) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","var getProto = Object.getPrototypeOf ? (obj) => (Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);\nvar leafPrototypes;\n// create a fake namespace object\n// mode & 1: value is a module id, require it\n// mode & 2: merge all properties of value into the ns\n// mode & 4: return value when already ns object\n// mode & 16: return value when it's Promise-like\n// mode & 8|1: behave like require\n__webpack_require__.t = function(value, mode) {\n\tif(mode & 1) value = this(value);\n\tif(mode & 8) return value;\n\tif(typeof value === 'object' && value) {\n\t\tif((mode & 4) && value.__esModule) return value;\n\t\tif((mode & 16) && typeof value.then === 'function') return value;\n\t}\n\tvar ns = Object.create(null);\n\t__webpack_require__.r(ns);\n\tvar def = {};\n\tleafPrototypes = leafPrototypes || [null, getProto({}), getProto([]), getProto(getProto)];\n\tfor(var current = mode & 2 && value; typeof current == 'object' && !~leafPrototypes.indexOf(current); current = getProto(current)) {\n\t\tObject.getOwnPropertyNames(current).forEach((key) => (def[key] = () => (value[key])));\n\t}\n\tdef['default'] = () => (value);\n\t__webpack_require__.d(ns, def);\n\treturn ns;\n};","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.f = {};\n// This file contains only the entry chunk.\n// The chunk loading function for additional chunks\n__webpack_require__.e = (chunkId) => {\n\treturn Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {\n\t\t__webpack_require__.f[key](chunkId, promises);\n\t\treturn promises;\n\t}, []));\n};","// This function allow to reference async chunks\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn undefined;\n};","// This function allow to reference async chunks\n__webpack_require__.miniCssF = (chunkId) => {\n\t// return url for filenames based on template\n\treturn undefined;\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","var inProgress = {};\nvar dataWebpackPrefix = \"_N_E:\";\n// loadScript function to load a script via script tag\n__webpack_require__.l = (url, done, key, chunkId) => {\n\tif(inProgress[url]) { inProgress[url].push(done); return; }\n\tvar script, needAttach;\n\tif(key !== undefined) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tfor(var i = 0; i < scripts.length; i++) {\n\t\t\tvar s = scripts[i];\n\t\t\tif(s.getAttribute(\"src\") == url || s.getAttribute(\"data-webpack\") == dataWebpackPrefix + key) { script = s; break; }\n\t\t}\n\t}\n\tif(!script) {\n\t\tneedAttach = true;\n\t\tscript = document.createElement('script');\n\n\t\tscript.charset = 'utf-8';\n\t\tscript.timeout = 120;\n\t\tif (__webpack_require__.nc) {\n\t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n\t\t}\n\t\tscript.setAttribute(\"data-webpack\", dataWebpackPrefix + key);\n\n\t\tscript.src = __webpack_require__.tu(url);\n\t}\n\tinProgress[url] = [done];\n\tvar onScriptComplete = (prev, event) => {\n\t\t// avoid mem leaks in IE.\n\t\tscript.onerror = script.onload = null;\n\t\tclearTimeout(timeout);\n\t\tvar doneFns = inProgress[url];\n\t\tdelete inProgress[url];\n\t\tscript.parentNode && script.parentNode.removeChild(script);\n\t\tdoneFns && doneFns.forEach((fn) => (fn(event)));\n\t\tif(prev) return prev(event);\n\t}\n\tvar timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);\n\tscript.onerror = onScriptComplete.bind(null, script.onerror);\n\tscript.onload = onScriptComplete.bind(null, script.onload);\n\tneedAttach && document.head.appendChild(script);\n};","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","var policy;\n__webpack_require__.tt = () => {\n\t// Create Trusted Type policy if Trusted Types are available and the policy doesn't exist yet.\n\tif (policy === undefined) {\n\t\tpolicy = {\n\t\t\tcreateScriptURL: (url) => (url)\n\t\t};\n\t\tif (typeof trustedTypes !== \"undefined\" && trustedTypes.createPolicy) {\n\t\t\tpolicy = trustedTypes.createPolicy(\"nextjs#bundler\", policy);\n\t\t}\n\t}\n\treturn policy;\n};","__webpack_require__.tu = (url) => (__webpack_require__.tt().createScriptURL(url));","__webpack_require__.p = \"/_next/\";","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t8068: 0,\n\t7513: 0,\n\t1973: 0\n};\n\n__webpack_require__.f.j = (chunkId, promises) => {\n\t\t// JSONP chunk loading for javascript\n\t\tvar installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;\n\t\tif(installedChunkData !== 0) { // 0 means \"already installed\".\n\n\t\t\t// a Promise means \"currently loading\".\n\t\t\tif(installedChunkData) {\n\t\t\t\tpromises.push(installedChunkData[2]);\n\t\t\t} else {\n\t\t\t\tif(!/^(1973|7513|8068)$/.test(chunkId)) {\n\t\t\t\t\t// setup Promise in chunk cache\n\t\t\t\t\tvar promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));\n\t\t\t\t\tpromises.push(installedChunkData[2] = promise);\n\n\t\t\t\t\t// start chunk loading\n\t\t\t\t\tvar url = __webpack_require__.p + __webpack_require__.u(chunkId);\n\t\t\t\t\t// create error before stack unwound to get useful stacktrace later\n\t\t\t\t\tvar error = new Error();\n\t\t\t\t\tvar loadingEnded = (event) => {\n\t\t\t\t\t\tif(__webpack_require__.o(installedChunks, chunkId)) {\n\t\t\t\t\t\t\tinstalledChunkData = installedChunks[chunkId];\n\t\t\t\t\t\t\tif(installedChunkData !== 0) installedChunks[chunkId] = undefined;\n\t\t\t\t\t\t\tif(installedChunkData) {\n\t\t\t\t\t\t\t\tvar errorType = event && (event.type === 'load' ? 'missing' : event.type);\n\t\t\t\t\t\t\t\tvar realSrc = event && event.target && event.target.src;\n\t\t\t\t\t\t\t\terror.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';\n\t\t\t\t\t\t\t\terror.name = 'ChunkLoadError';\n\t\t\t\t\t\t\t\terror.type = errorType;\n\t\t\t\t\t\t\t\terror.request = realSrc;\n\t\t\t\t\t\t\t\tinstalledChunkData[1](error);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t__webpack_require__.l(url, loadingEnded, \"chunk-\" + chunkId, chunkId);\n\t\t\t\t} else installedChunks[chunkId] = 0;\n\t\t\t}\n\t\t}\n};\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = (chunkId) => (installedChunks[chunkId] === 0);\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = (parentChunkLoadingFunction, data) => {\n\tvar [chunkIds, moreModules, runtime] = data;\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some((id) => (installedChunks[id] !== 0))) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunk_N_E\"] = self[\"webpackChunk_N_E\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","","",""],"names":[],"sourceRoot":"","ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circuitwall/jarela",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Andrew Ge Wu",
@@ -100,6 +100,7 @@
100
100
  "test:live:isolated:full": "node scripts/live-test-isolated.mjs --llm",
101
101
  "test:e2e": "playwright test",
102
102
  "test:e2e:ui": "playwright test --ui",
103
+ "promo:record": "node scripts/promo-record.mjs",
103
104
  "release:docker": "node scripts/release-docker.mjs",
104
105
  "release:docker:dry": "node scripts/release-docker.mjs --dry-run"
105
106
  },
package/CHANGELOG.md CHANGED
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.1] - 2026-06-09
11
+
12
+ A reliability + UX polish patch on top of 1.4.0. The PIN keypad used at
13
+ boot-time decrypt and screen-unlock has been collapsed into a single
14
+ component with two modes, the API client treats both 423 lock states
15
+ symmetrically (master-key-locked is no longer surfaced as a toast
16
+ error), and several silent failure modes uncovered during last
17
+ night's launcher diagnosis are now bounded and observable.
18
+
19
+ ### Changed
20
+
21
+ - **Unified PIN keypad**
22
+ ([#226](https://github.com/CircuitWall/jarela/pull/226)).
23
+ `UnlockScreen` and `ScreenLock` were near-duplicate 6-digit keypads;
24
+ collapsed into a single `PinKeypad` with `mode: "decrypt" | "unlock"`.
25
+ Both unlock paths converge on a shared `landOnAgentPicker()` so the
26
+ user always lands on the agent selector. Boot-time decrypt now uses
27
+ `router.refresh()` instead of a full `window.location.reload()`, so
28
+ the transition to the AppShell is seamless. The API client handles
29
+ 423 `locked` symmetrically with `screen-locked` — neither lock state
30
+ is surfaced as a toast error any more; the matching overlay mounts
31
+ instead.
32
+
33
+ ### Fixed
34
+
35
+ - **Bounded resource leaks and last-resort error handlers**
36
+ ([#227](https://github.com/CircuitWall/jarela/pull/227)). Installs
37
+ `process.on("uncaughtException")` and `process.on("unhandledRejection")`
38
+ in `instrumentation-node.ts` so stray async errors land in the in-memory
39
+ log ring instead of going to raw stderr or killing the server. The
40
+ thread-run SSE route's `cancel()` now tears down its 500 ms poll and
41
+ event subscriber on client disconnect; the provider-probe route clears
42
+ its timeout on the win-path; `lib/tools/async-results` exposes
43
+ `stopAsyncResults()` and the shutdown drain calls it. Gmail and GitHub
44
+ page-walkers switched from `Promise.all` to `Promise.allSettled` so one
45
+ failed item no longer bins the whole page. `AbortSignal.timeout()`
46
+ added to the dashboard currency lookups (Nominatim, restcountries,
47
+ open.er-api) and the GitHub-Copilot provider fetches.
48
+ - **Installer captures node stdio and bounds task retries**
49
+ ([#225](https://github.com/CircuitWall/jarela/pull/225)). Replaces
50
+ `Start-Process -RedirectStandardOutput/Error` (which silently dropped
51
+ both streams under `wscript -> powershell`) with a raw
52
+ `[System.Diagnostics.Process]` plus async `BeginOutputReadLine` /
53
+ `BeginErrorReadLine` writing through autoflushed `StreamWriter` s, so
54
+ `server.out.log` and `server.err.log` actually contain output. Logs
55
+ node's exit code on every cycle. Tightens the in-launcher rate limiter
56
+ from 5 to 3 restarts in 60 s. Reduces the scheduled-task retry policy
57
+ from `RestartCount=999, RestartInterval=1m` to `RestartCount=3,
58
+ RestartInterval=5m` — with an encrypted master key, every retry needs
59
+ manual PIN re-entry, so 999 silent retries was strictly worse than
60
+ failing loudly.
61
+ - **README promo video renders**
62
+ ([#223](https://github.com/CircuitWall/jarela/pull/223)). GitHub raw
63
+ was serving the audio-less `.webm` as `audio/webm` (mime-sniffer
64
+ mis-classifies); browsers refused to render it in a `<video>`.
65
+ Re-encoded to h264 mp4 (faststart, video-only) which raw.githubusercontent.com
66
+ tags as `video/mp4`, and switched the README to absolute raw URLs for
67
+ `src` and `poster` since the markdown sanitizer doesn't rewrite
68
+ relative paths inside `<video>`.
69
+
70
+ ## [1.4.0] - 2026-06-08
71
+
72
+ ### Added
73
+
74
+ - **Browser-extension element screenshot.** The page-capture flow now
75
+ ships a cropped PNG of the picked element alongside the text. The
76
+ content script grabs the visible viewport via
77
+ `chrome.tabs.captureVisibleTab` (loopback only, via the service worker)
78
+ and crops it to the element's bounding rect through `OffscreenCanvas`
79
+ at `devicePixelRatio`. The server validates the base64 payload (≤ 4 MB
80
+ encoded), persists the user message as a multipart `ContentPart[]` of
81
+ `[text, image]` so the bubble renders the picture inline, and
82
+ forwards the image part to the silent observer turn so vision-capable
83
+ agents see it on the immediate follow-up run. Falls back cleanly to
84
+ text-only capture if the snapshot is denied. See
85
+ [`docs/api.md`](./docs/api.md#post-apiv1page-capture) for the updated
86
+ request schema.
87
+ - **Promo video recorder.** `npm run promo:record` (via
88
+ [`scripts/promo-record.mjs`](./scripts/promo-record.mjs)) drives your
89
+ real local install in a 9:16 vertical PWA viewport and records a
90
+ dark-theme `.webm` of the tap-to-unlock intro, agent picker, a
91
+ human-paced chat turn, and a tour of every side panel. First run
92
+ saves auth state to `promo/.storage.json` and reuses it thereafter.
93
+
10
94
  ## [1.3.0] - 2026-06-08
11
95
 
12
96
  Two new agent capabilities and a hardening pass on tool wall-clocks.
package/README.md CHANGED
@@ -1,29 +1,31 @@
1
-
2
-
3
-
4
- <p align="center">
5
- <img src="./public/logo-mark-transparent.png" alt="Jarela" width="160" />
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./public/icon-512.png" />
4
+ <img src="./public/icon-512-light.png" alt="Jarela" width="140" />
5
+ </picture>
6
6
  </p>
7
7
 
8
8
  <h1 align="center">Jarela</h1>
9
9
 
10
10
  <p align="center">
11
- <b>A local-first, browser-based GUI for orchestrating multi-provider LLM agents.</b><br/>
12
- <sub>Next.js 16 + LangGraph + SQLite. PWA-installable. No cloud backend, no telemetry.</sub>
11
+ <b>A local-first, browser-based GUI for orchestrating multi-provider LLM agents.</b>
12
+ </p>
13
+ <p align="center">
14
+ <sub>Next.js 16 &middot; LangGraph &middot; SQLite &middot; PWA-installable &middot; no cloud backend, no telemetry</sub>
13
15
  </p>
14
16
 
15
17
  <p align="center">
16
- <a href="#quick-start">Quick start</a> ·
17
- <a href="#configuration-guide-home--work">Config guide</a> ·
18
- <a href="#supported-platforms">Platforms</a> ·
19
- <a href="#features">Features</a> ·
20
- <a href="#productivity-stacks-google--microsoft-at-parity">Google + Microsoft</a> ·
21
- <a href="#built-in-toolbelt">Tools</a> ·
22
- <a href="#providers">Providers</a> ·
23
- <a href="#connections">Connections</a> ·
24
- <a href="./docs/EXTENDING.md">Extending</a> ·
25
- <a href="./docs/ARCHITECTURE.md">Architecture</a> ·
26
- <a href="./CONTRIBUTING.md">Contributing</a> ·
18
+ <a href="#quick-start">Quick start</a> &middot;
19
+ <a href="#configuration-guide-home--work">Config guide</a> &middot;
20
+ <a href="#supported-platforms">Platforms</a> &middot;
21
+ <a href="#features">Features</a> &middot;
22
+ <a href="#productivity-stacks-google--microsoft-at-parity">Google + Microsoft</a> &middot;
23
+ <a href="#built-in-toolbelt">Tools</a> &middot;
24
+ <a href="#providers">Providers</a> &middot;
25
+ <a href="#connections">Connections</a> &middot;
26
+ <a href="./docs/EXTENDING.md">Extending</a> &middot;
27
+ <a href="./docs/ARCHITECTURE.md">Architecture</a> &middot;
28
+ <a href="./CONTRIBUTING.md">Contributing</a> &middot;
27
29
  <a href="#documentation">Docs</a>
28
30
  </p>
29
31
 
@@ -51,15 +53,16 @@
51
53
  </a>
52
54
  </p>
53
55
 
54
- ---
55
-
56
56
  <p align="center">
57
- <video src="https://github.com/user-attachments/assets/0f33f8d3-07bb-4850-9fcc-cfc97036f180" controls width="640" muted>
58
- Your browser doesn't support embedded video.
59
- <a href="https://github.com/user-attachments/assets/0f33f8d3-07bb-4850-9fcc-cfc97036f180">Download the clip</a>.
57
+ <video src="https://raw.githubusercontent.com/CircuitWall/jarela/main/docs/assets/jarela-promo.webm" poster="https://raw.githubusercontent.com/CircuitWall/jarela/main/docs/assets/jarela-promo-poster.jpg" autoplay loop muted playsinline controls width="320">
58
+ <img src="./docs/assets/jarela-promo-poster.jpg" alt="Jarela promo &mdash; PIN unlock, agent picker, chat, panel tour" width="320" />
60
59
  </video>
60
+ <br/>
61
+ <sub><i>Tap-to-unlock &rarr; agent picker &rarr; human-paced chat &rarr; full panel tour</i> &middot; <a href="./docs/assets/jarela-promo.webm">Download .webm</a></sub>
61
62
  </p>
62
63
 
64
+ ---
65
+
63
66
  ## Quick start
64
67
 
65
68
  Get to a working local agent in under 10 minutes:
@@ -239,9 +242,12 @@ create an Outlook Calendar invite in the same turn.
239
242
  **Memory**, **Documents**, **Profile**, **Bridges**, **Scheduled tasks**,
240
243
  and **Pending approvals**.
241
244
  - **Browser extension** ([`browser-extension/`](./browser-extension)) —
242
- Chrome MV3, click an element on any page and POST it to your local
243
- Jarela as a new user message (ADR-0018). Loopback only; toolbar icon
244
- greys out when Jarela isn't running.
245
+ Chrome MV3, click an element on any page and POST it (with a cropped
246
+ PNG of the picked element) to your local Jarela as a new user message
247
+ (ADR-0018). The screenshot is rendered inline in the chat bubble and
248
+ forwarded to vision-capable agents on the silent observer turn that
249
+ fires immediately after the capture. Loopback only; toolbar icon greys
250
+ out when Jarela isn't running.
245
251
 
246
252
  ### Operational
247
253
 
@@ -914,6 +920,25 @@ on every push and PR: `lint + tsc --noEmit + next build`, then the same
914
920
  live integration suite against the production server output. The build
915
921
  badge at the top of this README links straight to the latest run.
916
922
 
923
+ ## Recording a promo video
924
+
925
+ [scripts/promo-record.mjs](./scripts/promo-record.mjs) drives your real
926
+ local install (default `http://localhost:4312`) inside a 540&times;960
927
+ vertical (9:16) PWA viewport and records a `.webm` of a five-scene tour
928
+ in dark theme: a simulated PIN unlock, agent picker, a human-paced chat
929
+ turn, every side panel, and a closing pose.
930
+
931
+ ```bash
932
+ npm run dev # in one terminal
933
+ npm run promo:record # in another — output lands in ./promo/
934
+ ```
935
+
936
+ The first run opens a headed Chromium so you can manually unlock the
937
+ install if needed; the resulting auth state is saved to
938
+ `promo/.storage.json` and reused on every subsequent run. Override the
939
+ target with `JARELA_PROMO_URL`, the chat line with `JARELA_PROMO_MSG`,
940
+ or skip the actual send with `JARELA_PROMO_SKIP_CHAT=1`.
941
+
917
942
  ## Security
918
943
 
919
944
  - **CSRF / origin guard** ([lib/auth/access.ts](./lib/auth/access.ts))
package/api/client.ts CHANGED
@@ -157,20 +157,21 @@ async function request<T>(path: string, init?: RequestInit & { timeoutMs?: numbe
157
157
  signal: timeoutCtrl.signal,
158
158
  });
159
159
  if (!res.ok) {
160
- // 423 screen-locked: surface a window event so the AppShell can
161
- // mount the ScreenLock overlay. We still throw so the caller
162
- // sees the failure, but no point retrying the lock isn't
163
- // going to clear on its own.
160
+ // 423 lock states: distinct events so AppShell can mount the
161
+ // right overlay (decrypt vs presence-check). Both throw so the
162
+ // caller still sees the failure no point retrying, the lock
163
+ // isn't going to clear on its own.
164
164
  if (res.status === 423) {
165
165
  const cloned = res.clone();
166
166
  const body = (await cloned.json().catch(() => null)) as
167
167
  | { error?: string }
168
168
  | null;
169
- if (
170
- body?.error === "screen-locked" &&
171
- typeof window !== "undefined"
172
- ) {
173
- window.dispatchEvent(new CustomEvent("jarela:screen-locked"));
169
+ if (typeof window !== "undefined") {
170
+ if (body?.error === "screen-locked") {
171
+ window.dispatchEvent(new CustomEvent("jarela:screen-locked"));
172
+ } else if (body?.error === "locked") {
173
+ window.dispatchEvent(new CustomEvent("jarela:master-key-locked"));
174
+ }
174
175
  }
175
176
  throw new Error(`423 ${body?.error ?? "locked"}`);
176
177
  }
@@ -53,7 +53,8 @@ export async function GET(req: NextRequest) {
53
53
  source: "manual",
54
54
  updated_at: new Date().toISOString(),
55
55
  }, 3600);
56
- } catch {
56
+ } catch (err) {
57
+ console.warn("[currency] manual lookup failed, falling back to USD:", err);
57
58
  return cachedJson<CurrencyResponse>({
58
59
  currency: "USD",
59
60
  rate_from_usd: 1,
@@ -116,7 +117,8 @@ export async function GET(req: NextRequest) {
116
117
  source: "location",
117
118
  updated_at: new Date().toISOString(),
118
119
  }, 3600);
119
- } catch {
120
+ } catch (err) {
121
+ console.warn("[currency] location lookup failed, falling back to USD:", err);
120
122
  return cachedJson<CurrencyResponse>({
121
123
  currency: "USD",
122
124
  rate_from_usd: 1,
@@ -140,6 +142,7 @@ async function reverseCountryCode(lat: number, lng: number): Promise<string | nu
140
142
  "user-agent": "jarela-dashboard-currency/1.0",
141
143
  accept: "application/json",
142
144
  },
145
+ signal: AbortSignal.timeout(8_000),
143
146
  });
144
147
  if (!res.ok) return null;
145
148
  const body = await res.json() as {
@@ -158,6 +161,7 @@ async function resolveCurrencyForCountry(countryCode: string): Promise<string |
158
161
 
159
162
  const res = await fetch(`https://restcountries.com/v3.1/alpha/${countryCode}?fields=currencies`, {
160
163
  headers: { accept: "application/json" },
164
+ signal: AbortSignal.timeout(8_000),
161
165
  });
162
166
  if (!res.ok) return null;
163
167
  const body = await res.json() as Array<{ currencies?: Record<string, { name?: string }> }>;
@@ -173,6 +177,7 @@ async function fetchFxRates(): Promise<Record<string, number>> {
173
177
 
174
178
  const res = await fetch("https://open.er-api.com/v6/latest/USD", {
175
179
  headers: { accept: "application/json" },
180
+ signal: AbortSignal.timeout(8_000),
176
181
  });
177
182
  if (!res.ok) throw new Error(`fx status ${res.status}`);
178
183
 
@@ -66,14 +66,25 @@ export async function POST(req: NextRequest, { params }: Params) {
66
66
  try { await (iter as { return?: () => Promise<unknown> }).return?.(); } catch { /* best-effort */ }
67
67
  };
68
68
 
69
+ let timer: ReturnType<typeof setTimeout> | null = null;
69
70
  try {
70
71
  await Promise.race([
71
72
  probe(),
72
- new Promise((_, reject) => setTimeout(() => reject(new Error(`probe timed out after ${PROBE_TIMEOUT_MS}ms`)), PROBE_TIMEOUT_MS)),
73
+ new Promise<never>((_, reject) => {
74
+ timer = setTimeout(
75
+ () => reject(new Error(`probe timed out after ${PROBE_TIMEOUT_MS}ms`)),
76
+ PROBE_TIMEOUT_MS,
77
+ );
78
+ }),
73
79
  ]);
74
80
  return NextResponse.json({ ok: true });
75
81
  } catch (e) {
76
82
  const msg = e instanceof Error ? e.message : String(e);
77
83
  return NextResponse.json({ ok: false, error: msg });
84
+ } finally {
85
+ // Without this, the 15s timer keeps firing after probe() wins the
86
+ // race and the route returns — leaking an unhandled rejection per
87
+ // probe (which then trips the global handler).
88
+ if (timer) clearTimeout(timer);
78
89
  }
79
90
  }
@@ -233,9 +233,17 @@ function attachStream(
233
233
  thread_id: string,
234
234
  stream_options?: StreamOptions,
235
235
  ): Response {
236
+ // Captured by both start() and cancel() so the cancel branch can tear
237
+ // down the poll timer + subscription when the client disconnects.
238
+ // Without this, navigating away from a long-running thread leaks an
239
+ // event subscriber + 2Hz setInterval per visit until the run finishes
240
+ // (or forever if it doesn't).
241
+ let poll: ReturnType<typeof setInterval> | null = null;
242
+ let unsubscribe: (() => void) | null = null;
243
+ let clientGone = false;
244
+
236
245
  const stream = new ReadableStream({
237
246
  start(controller) {
238
- let clientGone = false;
239
247
  const safeEnqueue = (chunk: Uint8Array): void => {
240
248
  if (clientGone) return;
241
249
  try { controller.enqueue(chunk); } catch { clientGone = true; }
@@ -246,14 +254,15 @@ function attachStream(
246
254
  safeEnqueue(sse({ type: ev.type, ...ev.data }));
247
255
  };
248
256
 
249
- const { run, unsubscribe } = subscribe(thread_id, onEvent);
250
- if (!run) {
257
+ const sub = subscribe(thread_id, onEvent);
258
+ unsubscribe = sub.unsubscribe;
259
+ if (!sub.run) {
251
260
  controller.close();
252
261
  return;
253
262
  }
254
263
 
255
264
  // If run already terminal, close after replay.
256
- if (run.status !== "running") {
265
+ if (sub.run.status !== "running") {
257
266
  try { controller.close(); } catch { /* */ }
258
267
  return;
259
268
  }
@@ -261,11 +270,11 @@ function attachStream(
261
270
  // When the run finishes (status changes), close our response. We
262
271
  // poll lightly because the run might finish due to other subscribers'
263
272
  // signals; simpler than wiring a second listener channel.
264
- const poll = setInterval(() => {
273
+ poll = setInterval(() => {
265
274
  const r = getRun(thread_id);
266
275
  if (!r || r.status !== "running") {
267
- clearInterval(poll);
268
- unsubscribe();
276
+ if (poll) { clearInterval(poll); poll = null; }
277
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
269
278
  if (!clientGone) {
270
279
  try { controller.close(); } catch { /* */ }
271
280
  }
@@ -274,7 +283,12 @@ function attachStream(
274
283
  poll.unref?.();
275
284
  },
276
285
  cancel() {
277
- // Client navigated away — agent run keeps going in registry.
286
+ // Client navigated away — agent run keeps going in registry, but
287
+ // tear down OUR poll + subscription so we don't leak a timer +
288
+ // subscriber per disconnect.
289
+ clientGone = true;
290
+ if (poll) { clearInterval(poll); poll = null; }
291
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
278
292
  },
279
293
  });
280
294
 
@@ -73,6 +73,12 @@ function fileToContentPart(file: File): Promise<ContentPart> {
73
73
  });
74
74
  }
75
75
 
76
+ function attachmentKey(a: ContentPart, i: number): string {
77
+ if (a.type === "text") return `text:${i}:${a.text.length}`;
78
+ const name = a.type === "file" ? a.name : "";
79
+ return `${a.type}:${a.media_type}:${name}:${a.data.length}:${a.data.slice(0, 16)}`;
80
+ }
81
+
76
82
  export function InputBar({ attachments, onAttachmentsChange, onSubmit, onQueue, onStop, streaming, disabled, placeholder, voiceEnabled, agentId, onVoiceTranscript }: Props) {
77
83
  // Text state is intentionally LOCAL. Lifting it to ChatView would re-render
78
84
  // the entire message list (every MessageBubble + ReactMarkdown pass) on
@@ -268,7 +274,10 @@ export function InputBar({ attachments, onAttachmentsChange, onSubmit, onQueue,
268
274
  {attachments.length > 0 && (
269
275
  <div className="flex flex-wrap gap-2 mb-2">
270
276
  {attachments.map((a, i) => (
271
- <div key={i} className="relative group shrink-0">
277
+ // Content-derived key using the index reused DOM nodes when
278
+ // earlier attachments were removed, flashing the wrong preview
279
+ // (and the wrong filename) into the slot of the survivor.
280
+ <div key={attachmentKey(a, i)} className="relative group shrink-0">
272
281
  {a.type === "image" ? (
273
282
  // eslint-disable-next-line @next/next/no-img-element
274
283
  <img
@@ -31,6 +31,7 @@ import { Toaster } from "@/components/ui/Toaster";
31
31
  import { Logo } from "@/components/ui/Logo";
32
32
  import { BootScreen } from "@/components/ui/BootScreen";
33
33
  import { ScreenLock } from "@/components/setup/ScreenLock";
34
+ import { UnlockScreen } from "@/components/setup/UnlockScreen";
34
35
  import { clearUnreadForAgent, useUnreadCount } from "@/lib/ui/toasts";
35
36
  import { getAppName } from "@/lib/env/app-config";
36
37
  import { MenuPanel } from "./MenuPanel";
@@ -198,25 +199,31 @@ export function AppShell() {
198
199
  ? agents.find((a) => a.id === state.activeAgentId) ?? null
199
200
  : null;
200
201
 
201
- // Screen-lock overlay. Distinct from the boot-time master-key unlock
202
- // (that's gated server-side in `app/page.tsx`). This one is the
203
- // presence check that fires after `idle_timeout_ms` of inactivity:
204
- // background work keeps running but the UI is hidden until the user
205
- // re-enters their PIN. Triggered either by a 423 `screen-locked`
206
- // response from the api client or by the periodic state probe below.
202
+ // Screen-lock overlay (presence check) AND master-key-locked overlay
203
+ // (decrypt). Distinct from the boot-time gate in `app/page.tsx`:
204
+ // those mounts are triggered mid-session either by an idle timer
205
+ // (screen-lock) or by the master key being re-locked by an external
206
+ // process (decrypt). Both are signalled by the API client when it
207
+ // sees the matching 423 response.
207
208
  const [screenLocked, setScreenLocked] = useState(false);
209
+ const [masterKeyLocked, setMasterKeyLocked] = useState(false);
208
210
  // Bumped after each unlock so BootScreen remounts with fresh state
209
211
  // (its `done` / `pickedId` / `prefetchStartedRef` would otherwise
210
- // suppress the picker on the second appearance).
212
+ // suppress the picker on the second appearance). Both unlock paths
213
+ // bump this — the agent selector is always the post-unlock landing.
211
214
  const [bootSeq, setBootSeq] = useState(0);
212
215
  useEffect(() => {
213
216
  let cancelled = false;
214
217
  let timer: ReturnType<typeof setInterval> | null = null;
215
218
 
216
- function onLocked() {
219
+ function onScreenLocked() {
217
220
  if (!cancelled) setScreenLocked(true);
218
221
  }
219
- window.addEventListener("jarela:screen-locked", onLocked);
222
+ function onMasterKeyLocked() {
223
+ if (!cancelled) setMasterKeyLocked(true);
224
+ }
225
+ window.addEventListener("jarela:screen-locked", onScreenLocked);
226
+ window.addEventListener("jarela:master-key-locked", onMasterKeyLocked);
220
227
 
221
228
  // Soft poll every 30s so the overlay still appears if no user
222
229
  // action triggered a request after the idle timer elapsed.
@@ -224,12 +231,19 @@ export function AppShell() {
224
231
  try {
225
232
  const res = await fetch("/api/v1/security/state");
226
233
  if (!res.ok) return;
227
- const body = (await res.json()) as { screen_locked?: boolean };
228
- if (!cancelled && body.screen_locked === true) {
229
- setScreenLocked(true);
234
+ const body = (await res.json()) as {
235
+ screen_locked?: boolean;
236
+ state?: string;
237
+ };
238
+ if (cancelled) return;
239
+ if (body.state === "locked") setMasterKeyLocked(true);
240
+ if (body.screen_locked === true) setScreenLocked(true);
241
+ } catch (err) {
242
+ // Network blip; try again next tick. Logged at debug-level so
243
+ // a sustained outage is at least findable in devtools.
244
+ if (process.env.NODE_ENV !== "production") {
245
+ console.debug("[security/state] probe failed:", err);
230
246
  }
231
- } catch {
232
- // Network blip; try again next tick.
233
247
  }
234
248
  }
235
249
  void probe();
@@ -238,10 +252,20 @@ export function AppShell() {
238
252
  return () => {
239
253
  cancelled = true;
240
254
  if (timer) clearInterval(timer);
241
- window.removeEventListener("jarela:screen-locked", onLocked);
255
+ window.removeEventListener("jarela:screen-locked", onScreenLocked);
256
+ window.removeEventListener("jarela:master-key-locked", onMasterKeyLocked);
242
257
  };
243
258
  }, []);
244
259
 
260
+ // Shared post-unlock landing: clear the current chat and force the
261
+ // BootScreen to remount so the user lands on the agent picker. Used
262
+ // by BOTH the screen-unlock and the master-key decrypt paths so the
263
+ // two transitions feel identical from the user's side.
264
+ const landOnAgentPicker = useCallback(() => {
265
+ dispatch({ type: "NEW_CHAT" });
266
+ setBootSeq((n) => n + 1);
267
+ }, [dispatch]);
268
+
245
269
  return (
246
270
  // `dvh` natively tracks the visible viewport on iOS 16.4+ / modern
247
271
  // Chromium, including the on-screen keyboard. The `--actual-vh`
@@ -270,12 +294,24 @@ export function AppShell() {
270
294
  onUnlock={() => {
271
295
  // Drop the user back on the picker so they consciously
272
296
  // re-enter their workspace rather than landing mid-chat.
273
- dispatch({ type: "NEW_CHAT" });
274
- setBootSeq((n) => n + 1);
297
+ landOnAgentPicker();
275
298
  setScreenLocked(false);
276
299
  }}
277
300
  />
278
301
  )}
302
+ {masterKeyLocked && !screenLocked && (
303
+ // Master key got re-locked mid-session (e.g. external lock
304
+ // command). Mount the decrypt splash — same shared keypad as
305
+ // boot — and on success drop back to the agent picker. The
306
+ // existing components below stay mounted underneath; once
307
+ // unlocked they resume against the now-unlocked DB.
308
+ <UnlockScreen
309
+ onUnlock={() => {
310
+ landOnAgentPicker();
311
+ setMasterKeyLocked(false);
312
+ }}
313
+ />
314
+ )}
279
315
  <NotificationStatus />
280
316
  <Toaster />
281
317
  <ServerStatus />