@lazyingart/agintiflow 0.20.166 → 0.20.169

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": "@lazyingart/agintiflow",
3
- "version": "0.20.166",
3
+ "version": "0.20.169",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -52,6 +52,7 @@ const translations = {
52
52
  goalPlaceholder: "Open a site and summarize it, or use run_command for simple terminal inspection.",
53
53
  runDefaultsTitle: "Run defaults",
54
54
  runDefaultsHelp: "The chat box is the first goal for a new run. These controls set how that run starts.",
55
+ runDefaultsFoldHint: "Routing, safety, tool toggles, and key status.",
55
56
  scsModeLabel: "SCS",
56
57
  scsOnOption: "On",
57
58
  scsAutoOption: "Auto",
@@ -64,6 +65,8 @@ const translations = {
64
65
  allowedDomainsLabel: "Allowed domains",
65
66
  allowedDomainsPlaceholder: "news.ycombinator.com,github.com",
66
67
  commandCwdLabel: "Working directory",
68
+ commandCwdHelp: "Runs start from this working directory.",
69
+ commandCwdPlaceholder: "/home/user/project",
67
70
  maxStepsLabel: "Max steps",
68
71
  sandboxModeLabel: "Sandbox mode",
69
72
  permissionModeLabel: "Permission shortcut",
@@ -718,6 +721,8 @@ const setupApiKeyField = document.querySelector("#setup-api-key");
718
721
  const saveApiKeyButton = document.querySelector("#save-api-key");
719
722
  const setupStatusEl = document.querySelector("#setup-status");
720
723
  const taskProfileField = document.querySelector("#taskProfile");
724
+ const commandCwdField = document.querySelector("#commandCwd");
725
+ const commandCwdSuggestionsEl = document.querySelector("#command-cwd-suggestions");
721
726
  const permissionModeField = document.querySelector("#permissionMode");
722
727
  const permissionHintEl = document.querySelector("#permission-hint");
723
728
  const sandboxModeField = document.querySelector("#sandboxMode");
@@ -727,6 +732,7 @@ const logsEl = document.querySelector("#logs");
727
732
  const runMetaEl = document.querySelector("#run-meta");
728
733
  const stopRunButton = document.querySelector("#stop-run");
729
734
  const keyStatusEl = document.querySelector("#key-status");
735
+ const keyStatusListEl = document.querySelector("#key-status-list");
730
736
  const allowAuxiliaryToolsField = document.querySelector("#allowAuxiliaryTools");
731
737
  const allowWebSearchField = document.querySelector("#allowWebSearch");
732
738
  const allowMcpToolsField = document.querySelector("#allowMcpTools");
@@ -928,31 +934,99 @@ function setRunStatus(status = "", { sessionId, detail = "", announce = false }
928
934
  function renderKeyStatus(status = lastKeyStatus) {
929
935
  lastKeyStatus = status;
930
936
  if (!status) return;
931
- keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
932
- status.openai ? t("availableLabel") : t("missingLabel")
933
- } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · Qwen ${
934
- status.qwen ? t("availableLabel") : t("missingLabel")
935
- } · Venice ${
936
- status.venice ? t("availableLabel") : t("missingLabel")
937
- } · GRS AI ${
938
- status.grsai ? t("availableLabel") : t("missingLabel")
939
- } · ${t("mockLabel")} ${
940
- status.mock ? t("availableLabel") : t("missingLabel")
941
- }`;
937
+ const providers = [
938
+ ["OpenAI", status.openai],
939
+ ["DeepSeek", status.deepseek],
940
+ ["Qwen", status.qwen],
941
+ ["Venice", status.venice],
942
+ ["GRS AI", status.grsai],
943
+ [t("mockLabel"), status.mock],
944
+ ];
945
+ const available = providers.filter(([, ready]) => Boolean(ready)).length;
946
+ if (keyStatusEl) {
947
+ keyStatusEl.textContent = `${t("keysLabel")} ${available}/${providers.length}`;
948
+ keyStatusEl.dataset.ready = String(available > 0);
949
+ }
950
+ if (keyStatusListEl) {
951
+ keyStatusListEl.replaceChildren(
952
+ ...providers.map(([name, ready]) => {
953
+ const chip = document.createElement("div");
954
+ chip.className = "key-status-chip";
955
+ chip.dataset.ready = String(Boolean(ready));
956
+ const title = document.createElement("strong");
957
+ title.textContent = name;
958
+ const value = document.createElement("span");
959
+ value.textContent = ready ? t("availableLabel") : t("missingLabel");
960
+ chip.append(title, value);
961
+ return chip;
962
+ })
963
+ );
964
+ }
942
965
  if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek || status.qwen || status.venice);
943
966
  }
944
967
 
945
968
  function renderProjectStatus(info = projectInfo) {
946
969
  projectInfo = info;
947
970
  if (!projectStatusEl || !info) return;
948
- projectStatusEl.textContent = [
949
- info.platform?.label ? `os=${info.platform.label}` : "",
950
- `root=${info.root || ""}`,
951
- `cwd=${info.commandCwd || ""}`,
952
- `sessions=${info.sessionsDir || ""}`,
953
- `db=${info.sessionDbPath || ""}`,
954
- `shared=${info.sharedSessionFolder ? "yes" : "no"}`,
955
- ].join(" · ");
971
+ const rows = [
972
+ ["OS", info.platform?.label || ""],
973
+ ["Root", info.root || ""],
974
+ ["CWD", info.commandCwd || ""],
975
+ ["Sessions", info.sessionsDir || ""],
976
+ ["Database", info.sessionDbPath || ""],
977
+ ["Shared", info.sharedSessionFolder ? "yes" : "no"],
978
+ ].filter(([, value]) => value !== "");
979
+ projectStatusEl.replaceChildren(
980
+ ...rows.map(([label, value]) => {
981
+ const chip = document.createElement("div");
982
+ chip.className = "project-status-chip";
983
+ if (String(value).length > 54) chip.dataset.wide = "true";
984
+ const title = document.createElement("strong");
985
+ title.textContent = label;
986
+ const body = document.createElement("span");
987
+ body.textContent = value;
988
+ chip.append(title, body);
989
+ return chip;
990
+ })
991
+ );
992
+ }
993
+
994
+ function baseDirectorySuggestions(info = projectInfo) {
995
+ const projectLfsRoot = info?.root?.includes("/ProjectsLFS/")
996
+ ? `${info.root.split("/ProjectsLFS/")[0]}/ProjectsLFS`
997
+ : "";
998
+ const values = [
999
+ info?.commandCwd,
1000
+ info?.root,
1001
+ info?.sessionsDir ? info.sessionsDir.replace(/\/\.agintiflow\/sessions$/, "") : "",
1002
+ projectLfsRoot,
1003
+ ].filter(Boolean);
1004
+ return [...new Set(values)];
1005
+ }
1006
+
1007
+ function renderCommandCwdSuggestions(suggestions = []) {
1008
+ if (!commandCwdSuggestionsEl) return;
1009
+ commandCwdSuggestionsEl.replaceChildren(
1010
+ ...suggestions.slice(0, 16).map((value) => {
1011
+ const option = document.createElement("option");
1012
+ option.value = value;
1013
+ return option;
1014
+ })
1015
+ );
1016
+ }
1017
+
1018
+ async function refreshCommandCwdSuggestions(query = "") {
1019
+ const fallback = baseDirectorySuggestions();
1020
+ renderCommandCwdSuggestions(fallback);
1021
+ try {
1022
+ const response = await fetch(`/api/path-suggestions?q=${encodeURIComponent(query || "")}`);
1023
+ const data = await response.json();
1024
+ if (response.ok && Array.isArray(data.suggestions)) {
1025
+ renderCommandCwdSuggestions([...new Set([...data.suggestions, ...fallback])]);
1026
+ }
1027
+ } catch {
1028
+ renderCommandCwdSuggestions(fallback);
1029
+ }
956
1030
  }
957
1031
 
958
1032
  function renderTaskProfiles(selected = "auto") {
@@ -1584,7 +1658,7 @@ function formPayload() {
1584
1658
  auxiliaryModel: fieldValue(auxiliaryModelField) || "nano-banana-2",
1585
1659
  startUrl: document.querySelector("#startUrl").value.trim(),
1586
1660
  allowedDomains: document.querySelector("#allowedDomains").value.trim(),
1587
- commandCwd: document.querySelector("#commandCwd").value.trim(),
1661
+ commandCwd: commandCwdField?.value.trim() || "",
1588
1662
  maxSteps: Number(document.querySelector("#maxSteps").value) || 24,
1589
1663
  permissionMode: permissionModeField?.value || "normal",
1590
1664
  sandboxMode: sandboxModeField.value,
@@ -3146,6 +3220,22 @@ saveApiKeyButton?.addEventListener("click", async () => {
3146
3220
  form.addEventListener("input", schedulePreferenceSave);
3147
3221
  form.addEventListener("change", schedulePreferenceSave);
3148
3222
 
3223
+ let commandCwdSuggestTimer = null;
3224
+ commandCwdField?.addEventListener("focus", () => {
3225
+ refreshCommandCwdSuggestions(commandCwdField.value).catch(() => {});
3226
+ });
3227
+ commandCwdField?.addEventListener("input", () => {
3228
+ schedulePreferenceSave();
3229
+ clearTimeout(commandCwdSuggestTimer);
3230
+ commandCwdSuggestTimer = setTimeout(() => {
3231
+ refreshCommandCwdSuggestions(commandCwdField.value).catch(() => {});
3232
+ }, 120);
3233
+ });
3234
+ commandCwdField?.addEventListener("change", () => {
3235
+ schedulePreferenceSave();
3236
+ refreshCommandCwdSuggestions(commandCwdField.value).catch(() => {});
3237
+ });
3238
+
3149
3239
  chatInputEl.addEventListener("keydown", (event) => {
3150
3240
  if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
3151
3241
  event.preventDefault();
@@ -3343,7 +3433,7 @@ async function loadConfig() {
3343
3433
  renderTaskProfiles(prefs.taskProfile || "auto");
3344
3434
  document.querySelector("#startUrl").value = prefs.startUrl || "";
3345
3435
  document.querySelector("#allowedDomains").value = prefs.allowedDomains || "";
3346
- document.querySelector("#commandCwd").value = prefs.commandCwd || data.project?.root || "";
3436
+ if (commandCwdField) commandCwdField.value = prefs.commandCwd || data.project?.root || "";
3347
3437
  document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
3348
3438
  document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
3349
3439
  if (permissionModeField) permissionModeField.value = prefs.permissionMode || "normal";
@@ -3366,6 +3456,7 @@ async function loadConfig() {
3366
3456
 
3367
3457
  renderKeyStatus(data.keyStatus);
3368
3458
  renderProjectStatus(data.project);
3459
+ await refreshCommandCwdSuggestions(commandCwdField?.value || "");
3369
3460
  renderWrapperStatus(data.wrappers || []);
3370
3461
  refreshProviderDropdowns();
3371
3462
  renderWorkspacePanel(data.workspace, []);
package/public/index.html CHANGED
@@ -36,13 +36,30 @@
36
36
  <p class="subtle" data-i18n="intro">
37
37
  Project-aware, low-cost agent for real problems
38
38
  </p>
39
- <details class="project-card">
40
- <summary>
41
- <strong data-i18n="projectStatusTitle">Project folder</strong>
42
- <span class="project-card-toggle" aria-hidden="true"></span>
43
- </summary>
44
- <p id="project-status" class="subtle">Loading project context...</p>
45
- </details>
39
+ <section class="workspace-search-card" aria-labelledby="workspace-search-title">
40
+ <div class="workspace-search-head">
41
+ <div>
42
+ <strong id="workspace-search-title" data-i18n="projectStatusTitle">Project folder</strong>
43
+ <p class="subtle" data-i18n="commandCwdHelp">Runs start from this working directory.</p>
44
+ </div>
45
+ </div>
46
+ <label class="workspace-search-field">
47
+ <span data-i18n="commandCwdLabel">Working directory</span>
48
+ <input
49
+ id="commandCwd"
50
+ name="commandCwd"
51
+ type="search"
52
+ value="/home/lachlan/ProjectsLFS/Agent"
53
+ list="command-cwd-suggestions"
54
+ autocomplete="off"
55
+ spellcheck="false"
56
+ data-i18n-placeholder="commandCwdPlaceholder"
57
+ placeholder="/home/lachlan/ProjectsLFS/Agent"
58
+ />
59
+ <datalist id="command-cwd-suggestions"></datalist>
60
+ </label>
61
+ <div id="project-status" class="project-status-grid" aria-live="polite">Loading project context...</div>
62
+ </section>
46
63
 
47
64
  <section id="setup-card" class="setup-card" hidden>
48
65
  <div>
@@ -84,14 +101,20 @@
84
101
  </div>
85
102
  </section>
86
103
 
87
- <form id="run-form" class="run-defaults-form">
88
- <div class="run-defaults-head">
89
- <div>
104
+ <details id="run-defaults-card" class="run-defaults-card">
105
+ <summary class="run-defaults-summary">
106
+ <span>
90
107
  <strong data-i18n="runDefaultsTitle">Run defaults</strong>
108
+ <span class="subtle" data-i18n="runDefaultsFoldHint">Routing, safety, tool toggles, and key status.</span>
109
+ </span>
110
+ <span id="key-status" class="key-status-summary"></span>
111
+ <span class="project-card-toggle" aria-hidden="true"></span>
112
+ </summary>
113
+ <form id="run-form" class="run-defaults-form">
114
+ <div class="run-defaults-head">
91
115
  <p class="subtle" data-i18n="runDefaultsHelp">The chat box is the first goal for a new run. These controls set how that run starts.</p>
116
+ <div id="key-status-list" class="key-status-list" aria-live="polite"></div>
92
117
  </div>
93
- <span id="key-status" class="subtle"></span>
94
- </div>
95
118
 
96
119
  <label>
97
120
  <span data-i18n="routingModeLabel">Routing policy</span>
@@ -135,10 +158,6 @@
135
158
  </label>
136
159
 
137
160
  <div class="grid">
138
- <label>
139
- <span data-i18n="commandCwdLabel">Working directory</span>
140
- <input id="commandCwd" name="commandCwd" type="text" value="/home/lachlan/ProjectsLFS/Agent" />
141
- </label>
142
161
  <label>
143
162
  <span data-i18n="maxStepsLabel">Max steps</span>
144
163
  <input id="maxSteps" name="maxSteps" type="number" min="1" max="50" value="24" />
@@ -202,7 +221,8 @@
202
221
  </div>
203
222
 
204
223
  <button id="open-settings" type="button" class="secondary settings-button">Advanced settings</button>
205
- </form>
224
+ </form>
225
+ </details>
206
226
  </section>
207
227
 
208
228
  <section class="stack">
package/public/styles.css CHANGED
@@ -131,6 +131,108 @@ h1 {
131
131
  .run-defaults-form {
132
132
  display: grid;
133
133
  gap: 14px;
134
+ margin-top: 12px;
135
+ }
136
+
137
+ .run-defaults-card {
138
+ min-width: 0;
139
+ margin-top: 16px;
140
+ padding: 12px;
141
+ border: 1px solid rgba(15, 118, 110, 0.18);
142
+ border-radius: 18px;
143
+ background:
144
+ linear-gradient(135deg, rgba(236, 253, 245, 0.72), rgba(255, 253, 250, 0.9)),
145
+ #fffdfa;
146
+ }
147
+
148
+ .run-defaults-summary {
149
+ display: grid;
150
+ grid-template-columns: minmax(0, 1fr) max-content max-content;
151
+ gap: 10px;
152
+ align-items: center;
153
+ list-style: none;
154
+ cursor: pointer;
155
+ }
156
+
157
+ .run-defaults-summary::-webkit-details-marker {
158
+ display: none;
159
+ }
160
+
161
+ .run-defaults-summary strong,
162
+ .workspace-search-head strong {
163
+ display: block;
164
+ color: #0f766e;
165
+ text-transform: uppercase;
166
+ letter-spacing: 0.08em;
167
+ }
168
+
169
+ .run-defaults-summary .subtle {
170
+ display: block;
171
+ margin-top: 2px;
172
+ font-size: 0.78rem;
173
+ }
174
+
175
+ .key-status-summary {
176
+ display: inline-flex;
177
+ min-height: 32px;
178
+ align-items: center;
179
+ padding: 5px 10px;
180
+ border: 1px solid rgba(15, 118, 110, 0.18);
181
+ border-radius: 999px;
182
+ background: rgba(255, 253, 250, 0.82);
183
+ color: var(--muted);
184
+ font-size: 0.78rem;
185
+ font-weight: 850;
186
+ white-space: nowrap;
187
+ }
188
+
189
+ .key-status-summary[data-ready="true"] {
190
+ border-color: rgba(15, 118, 110, 0.32);
191
+ background: rgba(204, 251, 241, 0.52);
192
+ color: #0f766e;
193
+ }
194
+
195
+ .key-status-list,
196
+ .project-status-grid {
197
+ display: grid;
198
+ gap: 8px;
199
+ }
200
+
201
+ .key-status-list {
202
+ grid-template-columns: repeat(auto-fit, minmax(116px, 1fr));
203
+ }
204
+
205
+ .key-status-chip,
206
+ .project-status-chip {
207
+ min-width: 0;
208
+ padding: 9px 10px;
209
+ border: 1px solid rgba(95, 86, 74, 0.16);
210
+ border-radius: 14px;
211
+ background: rgba(255, 253, 250, 0.78);
212
+ }
213
+
214
+ .key-status-chip[data-ready="true"] {
215
+ border-color: rgba(15, 118, 110, 0.28);
216
+ background: rgba(204, 251, 241, 0.42);
217
+ }
218
+
219
+ .key-status-chip strong,
220
+ .project-status-chip strong {
221
+ display: block;
222
+ color: #0f766e;
223
+ font-size: 0.72rem;
224
+ letter-spacing: 0.06em;
225
+ text-transform: uppercase;
226
+ }
227
+
228
+ .key-status-chip span,
229
+ .project-status-chip span {
230
+ display: block;
231
+ min-width: 0;
232
+ overflow-wrap: anywhere;
233
+ color: var(--muted);
234
+ font-size: 0.8rem;
235
+ line-height: 1.35;
134
236
  }
135
237
 
136
238
  .run-defaults-head {
@@ -142,14 +244,8 @@ h1 {
142
244
  background: rgba(204, 251, 241, 0.28);
143
245
  }
144
246
 
145
- .run-defaults-head strong {
146
- color: #0f766e;
147
- text-transform: uppercase;
148
- letter-spacing: 0.08em;
149
- }
150
-
151
247
  .run-defaults-head p {
152
- margin: 4px 0 0;
248
+ margin: 0;
153
249
  font-size: 0.88rem;
154
250
  line-height: 1.45;
155
251
  }
@@ -316,6 +412,7 @@ h1 {
316
412
  background: rgba(204, 251, 241, 0.52);
317
413
  }
318
414
 
415
+ .workspace-search-card,
319
416
  .project-card,
320
417
  .setup-card {
321
418
  min-width: 0;
@@ -331,6 +428,61 @@ h1 {
331
428
  gap: 10px;
332
429
  }
333
430
 
431
+ .workspace-search-card {
432
+ display: grid;
433
+ gap: 12px;
434
+ margin-top: 16px;
435
+ }
436
+
437
+ .workspace-search-head {
438
+ display: flex;
439
+ min-width: 0;
440
+ gap: 12px;
441
+ align-items: start;
442
+ justify-content: space-between;
443
+ }
444
+
445
+ .workspace-search-head p {
446
+ margin: 3px 0 0;
447
+ line-height: 1.45;
448
+ }
449
+
450
+ .workspace-search-field {
451
+ position: relative;
452
+ display: grid;
453
+ gap: 6px;
454
+ }
455
+
456
+ .workspace-search-field input[type="search"] {
457
+ min-height: 48px;
458
+ padding: 12px 14px 12px 42px;
459
+ border-color: rgba(15, 118, 110, 0.24);
460
+ border-radius: 999px;
461
+ background:
462
+ linear-gradient(90deg, rgba(204, 251, 241, 0.28), rgba(255, 253, 250, 0.96)),
463
+ #fffdfa;
464
+ font-size: 0.94rem;
465
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
466
+ }
467
+
468
+ .workspace-search-field::before {
469
+ position: absolute;
470
+ bottom: 12px;
471
+ left: 15px;
472
+ color: #0f766e;
473
+ font-size: 1rem;
474
+ content: "⌕";
475
+ pointer-events: none;
476
+ }
477
+
478
+ .project-status-grid {
479
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
480
+ }
481
+
482
+ .project-status-chip[data-wide="true"] {
483
+ grid-column: 1 / -1;
484
+ }
485
+
334
486
  .project-card {
335
487
  display: block;
336
488
  }
@@ -554,13 +706,18 @@ textarea {
554
706
  }
555
707
 
556
708
  .compact-switch {
557
- min-height: 42px;
709
+ width: max-content;
710
+ grid-template-columns: auto auto;
711
+ height: 50px;
712
+ min-height: 50px;
558
713
  padding: 8px 10px;
714
+ gap: 8px;
715
+ white-space: nowrap;
559
716
  }
560
717
 
561
718
  .compact-switch .switch {
562
- width: 50px;
563
- min-width: 50px;
719
+ width: 44px;
720
+ min-width: 44px;
564
721
  }
565
722
 
566
723
  .compact-switch .switch::before {
@@ -568,7 +725,7 @@ textarea {
568
725
  }
569
726
 
570
727
  .compact-switch input[type="checkbox"]:checked + .switch::before {
571
- transform: translateX(24px);
728
+ transform: translateX(18px);
572
729
  }
573
730
 
574
731
  .settings-button {
@@ -644,7 +801,7 @@ button.danger {
644
801
 
645
802
  .quick-mode-bar {
646
803
  display: grid;
647
- grid-template-columns: minmax(130px, 170px) repeat(2, minmax(110px, max-content)) minmax(0, 1fr);
804
+ grid-template-columns: max-content max-content max-content minmax(0, 1fr);
648
805
  gap: 10px;
649
806
  align-items: center;
650
807
  padding: 10px;
@@ -656,8 +813,16 @@ button.danger {
656
813
  }
657
814
 
658
815
  .quick-mode-select {
659
- display: grid;
660
- gap: 5px;
816
+ display: inline-flex;
817
+ width: max-content;
818
+ height: 50px;
819
+ min-height: 50px;
820
+ align-items: center;
821
+ gap: 8px;
822
+ padding: 8px 10px;
823
+ border: 1px solid var(--line);
824
+ border-radius: 14px;
825
+ background: rgba(255, 253, 250, 0.72);
661
826
  }
662
827
 
663
828
  .quick-mode-select span {
@@ -668,6 +833,13 @@ button.danger {
668
833
  text-transform: uppercase;
669
834
  }
670
835
 
836
+ .quick-mode-select select {
837
+ width: 68px;
838
+ min-width: 68px;
839
+ padding: 5px 20px 5px 9px;
840
+ border-radius: 10px;
841
+ }
842
+
671
843
  .quick-mode-status {
672
844
  min-width: 0;
673
845
  overflow: hidden;
@@ -1851,6 +2023,14 @@ button.danger {
1851
2023
  grid-template-columns: 1fr;
1852
2024
  }
1853
2025
 
2026
+ .run-defaults-summary {
2027
+ grid-template-columns: 1fr max-content;
2028
+ }
2029
+
2030
+ .run-defaults-summary .key-status-summary {
2031
+ justify-self: start;
2032
+ }
2033
+
1854
2034
  .quick-mode-status {
1855
2035
  white-space: normal;
1856
2036
  }
@@ -15,6 +15,7 @@ import {
15
15
  classifyEscapeAction,
16
16
  formatElapsedDuration,
17
17
  formatWorkspaceChange,
18
+ shouldIgnorePromptSubmission,
18
19
  stripMarkdown,
19
20
  } from "../src/interactive-cli.js";
20
21
  import { formatSessionChoices, parseArgs, parseResumeCommandArgs, splitResumeCommandArgv } from "../src/cli.js";
@@ -218,6 +219,60 @@ async function runTmuxInterruptSmoke({ key, expected }) {
218
219
  }
219
220
  }
220
221
 
222
+ async function runTmuxEmptyEnterSmoke() {
223
+ const session = `aginti-empty-enter-${process.pid}`;
224
+ const command = [
225
+ "env",
226
+ `AGINTIFLOW_HOME=${shellQuote(agintiflowHome)}`,
227
+ "AGINTIFLOW_RUNTIME_DIR=",
228
+ "AGINTIFLOW_NO_WEB_AUTO_START=1",
229
+ "AGINTI_LANGUAGE=en",
230
+ shellQuote(process.execPath),
231
+ shellQuote(binPath),
232
+ "chat",
233
+ "--provider",
234
+ "mock",
235
+ "--routing",
236
+ "manual",
237
+ "--profile",
238
+ "code",
239
+ "--sandbox-mode",
240
+ "host",
241
+ ].join(" ");
242
+ const shellCommand = `cd ${shellQuote(tempRoot)} && ${command}`;
243
+ try {
244
+ tmux(["kill-session", "-t", session], { timeout: 2000 });
245
+ } catch {
246
+ // Session does not exist.
247
+ }
248
+ try {
249
+ tmux(["new-session", "-d", "-s", session, "bash", "-lc", `${shellCommand}; printf '\\nEXIT:%s\\n' "$?"; sleep 3`]);
250
+ const before = await waitForTmuxText(session, /user>/, 10000);
251
+ const beforeCount = (before.match(/^\s*user>/gm) || []).length;
252
+ if (beforeCount < 1) throw new Error(`empty-enter smoke did not reach the composer\n${before}`);
253
+ tmux(["send-keys", "-t", session, "Enter"]);
254
+ await new Promise((resolve) => setTimeout(resolve, 750));
255
+ const after = tmuxCapture(session);
256
+ const afterCount = (after.match(/^\s*user>/gm) || []).length;
257
+ if (afterCount !== beforeCount) {
258
+ throw new Error(`empty Enter committed a blank user turn: before=${beforeCount} after=${afterCount}\n${after}`);
259
+ }
260
+ if (/Mock run complete|status=running|status=idle session=/.test(after)) {
261
+ throw new Error(`empty Enter started an agent run\n${after}`);
262
+ }
263
+ tmux(["send-keys", "-t", session, "-l", "/exit"]);
264
+ tmux(["send-keys", "-t", session, "Enter"]);
265
+ const exited = await waitForTmuxText(session, /EXIT:0/, 8000);
266
+ if (!/EXIT:0/.test(exited)) throw new Error(`empty-enter smoke did not exit cleanly\n${exited}`);
267
+ } finally {
268
+ try {
269
+ tmux(["kill-session", "-t", session], { timeout: 2000 });
270
+ } catch {
271
+ // Session may already have exited.
272
+ }
273
+ }
274
+ }
275
+
221
276
  try {
222
277
  const translatedHelpKeys = [
223
278
  "helpHelp",
@@ -607,6 +662,9 @@ try {
607
662
  ) {
608
663
  throw new Error("slash command prompt canonicalization did not preserve final submitted command correctly");
609
664
  }
665
+ if (!shouldIgnorePromptSubmission("") || !shouldIgnorePromptSubmission(" \n\t ") || shouldIgnorePromptSubmission("run analysis")) {
666
+ throw new Error("empty prompt submission guard did not classify blank input correctly");
667
+ }
610
668
  const scsBooleanArgs = parseArgs(["--scs", "fix this project"]);
611
669
  if (scsBooleanArgs.enableScs !== "on" || scsBooleanArgs.goal !== "fix this project") {
612
670
  throw new Error("--scs should behave as a boolean flag when followed by a task");
@@ -865,6 +923,7 @@ try {
865
923
 
866
924
  await runTmuxInterruptSmoke({ key: "Escape", expected: /Active run stopped|Session saved/ });
867
925
  await runTmuxInterruptSmoke({ key: "C-c", expected: /EXIT:130|Session stopped by ctrl-c|Interrupted\. Session saved/ });
926
+ await runTmuxEmptyEnterSmoke();
868
927
 
869
928
  console.log(
870
929
  JSON.stringify(
@@ -909,6 +968,7 @@ try {
909
968
  "one-shot-cwd",
910
969
  "tmux-escape-active-run-stop",
911
970
  "tmux-ctrl-c-session-stop",
971
+ "tmux-empty-enter-noop",
912
972
  "resume-latest",
913
973
  "resume-history-metadata",
914
974
  "resume-history-prompt-labels",
@@ -48,7 +48,7 @@ async function fetchJson(pathname, options = {}) {
48
48
  }
49
49
 
50
50
  async function waitForHealth() {
51
- const deadline = Date.now() + 15000;
51
+ const deadline = Date.now() + 30000;
52
52
  while (Date.now() < deadline) {
53
53
  if (server.exitCode !== null) break;
54
54
  try {
@@ -96,7 +96,7 @@ try {
96
96
  if (chatThreadIndex < 0 || chatPendingIndex < 0 || chatPendingIndex < chatThreadIndex) {
97
97
  throw new Error("pending message panel must render below the chat thread");
98
98
  }
99
- for (const marker of ['id="new-session"', 'id="run-state"', 'id="toast-region"', 'id="chat-submit"', 'id="enableScs"', 'id="aapsModeToggle"', 'id="veniceModeToggle"', 'id="dynamicSteps"']) {
99
+ for (const marker of ['id="new-session"', 'id="run-state"', 'id="toast-region"', 'id="chat-submit"', 'id="command-cwd-suggestions"', 'id="enableScs"', 'id="aapsModeToggle"', 'id="veniceModeToggle"', 'id="dynamicSteps"']) {
100
100
  if (!webAppHtml.includes(marker)) throw new Error(`web UI is missing ${marker}`);
101
101
  }
102
102
  if (webAppHtml.includes('id="goal"') || /<button[^>]+type="submit"[^>]*>\s*Start run\s*<\/button>/i.test(webAppHtml)) {
@@ -109,6 +109,10 @@ try {
109
109
  if (config.preferences?.preferredWrapper !== "codex") throw new Error("Codex is not the default preferred wrapper");
110
110
  if (config.project?.root !== runtimeDir) throw new Error("web project root did not default to launch directory");
111
111
  if (config.preferences?.commandCwd !== runtimeDir) throw new Error("commandCwd did not default to project root");
112
+ const pathSuggest = await fetchJson(`/api/path-suggestions?q=${encodeURIComponent(runtimeDir.slice(0, runtimeDir.lastIndexOf("/")))}`);
113
+ if (!pathSuggest.ok || !Array.isArray(pathSuggest.suggestions) || !pathSuggest.suggestions.some((item) => item === runtimeDir)) {
114
+ throw new Error(`path suggestions did not include the runtime directory: ${JSON.stringify(pathSuggest)}`);
115
+ }
112
116
  if (config.preferences?.sandboxMode !== "docker-workspace") throw new Error("web did not default to docker workspace");
113
117
  if (config.preferences?.packageInstallPolicy !== "allow") throw new Error("web did not default to Docker package installs");
114
118
  if (config.preferences?.permissionMode !== "normal") throw new Error("web did not default to normal permission mode");
@@ -88,6 +88,12 @@ try {
88
88
  if (!/start new run/i.test(initialSubmit)) throw new Error(`composer did not default to new-run mode: ${initialSubmit}`);
89
89
  if (await page.locator("#stop-run").isVisible()) throw new Error("stop button is visible before a run starts");
90
90
  if ((await page.locator("#goal").count()) !== 0) throw new Error("old standalone goal textarea is still rendered");
91
+ if (!(await page.locator("#commandCwd").isVisible())) throw new Error("working directory search field is not visible at the top level");
92
+ if (await page.locator("#run-defaults-card").evaluate((node) => node.open)) throw new Error("run defaults should start folded");
93
+ await page.locator("#commandCwd").fill(runtimeDir.slice(0, Math.max(runtimeDir.lastIndexOf("/"), 1)));
94
+ await page.waitForFunction(() => document.querySelectorAll("#command-cwd-suggestions option").length > 0);
95
+ if ((await page.locator(".project-status-chip").count()) < 4) throw new Error("project folder status did not render structured chips");
96
+ await page.locator("#run-defaults-card summary").click();
91
97
 
92
98
  await page.selectOption("#enableScs", "auto");
93
99
  await page.locator("label:has(#aapsModeToggle)").click();
@@ -99,6 +105,19 @@ try {
99
105
  if (!/scs=auto/.test(quickStatus) || !/profile=aaps/.test(quickStatus) || !/route venice\//.test(quickStatus)) {
100
106
  throw new Error(`quick mode status did not reflect CLI modes: ${quickStatus}`);
101
107
  }
108
+ const quickBoxes = await Promise.all([
109
+ page.locator(".quick-mode-select").boundingBox(),
110
+ page.locator("label:has(#aapsModeToggle)").boundingBox(),
111
+ page.locator("label:has(#veniceModeToggle)").boundingBox(),
112
+ ]);
113
+ if (quickBoxes.some((box) => !box)) throw new Error("quick mode controls did not render measurable boxes");
114
+ const centers = quickBoxes.map((box) => box.y + box.height / 2);
115
+ if (Math.max(...centers) - Math.min(...centers) > 2) {
116
+ throw new Error(`quick mode controls are not vertically aligned: ${JSON.stringify(quickBoxes)}`);
117
+ }
118
+ if (quickBoxes.some((box) => box.height < 48 || box.height > 52) || quickBoxes.some((box) => box.width > 128)) {
119
+ throw new Error(`quick mode controls are not compact button-sized elements: ${JSON.stringify(quickBoxes)}`);
120
+ }
102
121
 
103
122
  await page.selectOption("#routingMode", "manual");
104
123
  await page.selectOption("#provider", "mock");
@@ -154,7 +173,11 @@ try {
154
173
  checks: [
155
174
  "composer-starts-new-run",
156
175
  "old-goal-form-hidden",
176
+ "working-directory-search-top",
177
+ "project-status-chips",
178
+ "run-defaults-folded",
157
179
  "quick-scs-aaps-venice-controls",
180
+ "quick-mode-control-alignment",
158
181
  "dynamic-steps-dropdown",
159
182
  "composer-goal-payload",
160
183
  "running-stop-button-visible",
@@ -18,8 +18,15 @@ function occurrenceCount(value, pattern) {
18
18
  return String(value || "").split(pattern).length - 1;
19
19
  }
20
20
 
21
+ function latestWebappEvent(value, statePattern = "started|restarted|reused|stopped") {
22
+ const pattern = new RegExp(`webapp=(http://127\\.0\\.0\\.1:(\\d+)) (${statePattern})`, "g");
23
+ const matches = [...String(value || "").matchAll(pattern)];
24
+ const latest = matches.at(-1);
25
+ return latest ? { url: latest[1], port: Number(latest[2]), state: latest[3] } : null;
26
+ }
27
+
21
28
  async function waitFor(predicate, child, label, output) {
22
- const deadline = Date.now() + 12000;
29
+ const deadline = Date.now() + 30000;
23
30
  while (Date.now() < deadline) {
24
31
  if (predicate()) return true;
25
32
  if (child.exitCode !== null) break;
@@ -67,12 +74,15 @@ async function runCase({ port, env = {}, expectHeader, label }) {
67
74
 
68
75
  try {
69
76
  await waitFor(() => output.stdout.includes(expectHeader), child, `${label} launch header`, output);
77
+ await waitFor(() => output.stdout.includes("status=idle") && output.stdout.includes("user>"), child, `${label} interactive ready`, output);
70
78
  child.stdin.write(`/webapp ${port}\n`);
71
- await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port}`), child, `${label} /webapp command`, output);
72
- child.stdin.write(`/webapp restart ${port}\n`);
73
- await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} restarted`), child, `${label} /webapp restart command`, output);
74
- const health = await fetch(`http://127.0.0.1:${port}/health`).then((response) => response.json());
75
- if (!health.ok || health.app !== "agintiflow" || Number(health.port) !== port) {
79
+ await waitFor(() => latestWebappEvent(output.stdout, "started|reused"), child, `${label} /webapp command`, output);
80
+ let active = latestWebappEvent(output.stdout, "started|reused");
81
+ child.stdin.write(`/webapp restart ${active.port}\n`);
82
+ await waitFor(() => latestWebappEvent(output.stdout, "restarted"), child, `${label} /webapp restart command`, output);
83
+ active = latestWebappEvent(output.stdout, "restarted");
84
+ const health = await fetch(`${active.url}/health`).then((response) => response.json());
85
+ if (!health.ok || health.app !== "agintiflow" || Number(health.port) !== active.port) {
76
86
  throw new Error(`invalid /webapp health response for ${label}: ${JSON.stringify(health)}`);
77
87
  }
78
88
  if (path.resolve(health.agintiflowHome) !== path.resolve(path.join(runtimeDir, `.agintiflow-web-home-${label}`))) {
@@ -85,19 +95,19 @@ async function runCase({ port, env = {}, expectHeader, label }) {
85
95
  await waitFor(() => occurrenceCount(output.stdout, "webapp auto-start=disabled") > disabledCount, child, `${label} /webapp status disabled command`, output);
86
96
  child.stdin.write(`/webapp enable\n`);
87
97
  await waitFor(() => output.stdout.includes("webapp auto-start=enabled"), child, `${label} /webapp enable command`, output);
88
- child.stdin.write(`/webapp stop ${port}\n`);
89
- await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} stopped`), child, `${label} /webapp stop command`, output);
98
+ child.stdin.write(`/webapp stop ${active.port}\n`);
99
+ await waitFor(() => latestWebappEvent(output.stdout, "stopped")?.port === active.port, child, `${label} /webapp stop command`, output);
90
100
  let stopped = false;
91
101
  try {
92
- await fetch(`http://127.0.0.1:${port}/health`);
102
+ await fetch(`${active.url}/health`);
93
103
  } catch {
94
104
  stopped = true;
95
105
  }
96
106
  if (!stopped) {
97
107
  throw new Error(`webapp still responded after /webapp stop for ${label}`);
98
108
  }
99
- child.stdin.write(`/webapp ${port}\n`);
100
- await waitFor(() => output.stdout.includes(`webapp=http://127.0.0.1:${port} started`), child, `${label} /webapp restart after stop`, output);
109
+ child.stdin.write(`/webapp ${active.port}\n`);
110
+ await waitFor(() => latestWebappEvent(output.stdout, "started|reused")?.port === active.port, child, `${label} /webapp restart after stop`, output);
101
111
  } finally {
102
112
  child.kill("SIGTERM");
103
113
  await killPort(port);
@@ -539,6 +539,10 @@ export function canonicalSlashPromptBuffer(value = "") {
539
539
  return resolved === raw ? text : `/${resolved}`;
540
540
  }
541
541
 
542
+ export function shouldIgnorePromptSubmission(value = "") {
543
+ return canonicalSlashPromptBuffer(value).trim().length === 0;
544
+ }
545
+
542
546
  function clamp(value, min, max) {
543
547
  return Math.min(Math.max(value, min), max);
544
548
  }
@@ -1487,6 +1491,18 @@ function readTtyPrompt(options = {}) {
1487
1491
  buffer = canonical;
1488
1492
  cursor = buffer.length;
1489
1493
  }
1494
+ if (shouldIgnorePromptSubmission(buffer)) {
1495
+ if (buffer.length > 0 || cursor !== 0) {
1496
+ buffer = "";
1497
+ cursor = 0;
1498
+ preferredColumn = null;
1499
+ suggestionAnchor = "";
1500
+ suggestionIndex = 0;
1501
+ promptHistory.resetBrowsing();
1502
+ redraw();
1503
+ }
1504
+ return;
1505
+ }
1490
1506
  clearPromptPanel();
1491
1507
  cleanup();
1492
1508
  printCommittedUserInput(buffer);
@@ -138,7 +138,8 @@ function compatibleHealth(health = {}, { cwd = "", home = "", packageDir = "" }
138
138
  if (!health.runtimeDir || !health.agintiflowHome) return false;
139
139
  if (!samePath(health.runtimeDir, cwd)) return false;
140
140
  if (!samePath(health.agintiflowHome, home)) return false;
141
- if (health.packageDir && packageDir && !samePath(health.packageDir, packageDir)) return false;
141
+ // Package paths legitimately change after npm updates or local installs. The
142
+ // safe ownership boundary for reuse/stop is the same app, runtime, and home.
142
143
  return true;
143
144
  }
144
145
 
package/web.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createReadStream } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import express from "express";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { runAgent } from "./src/agent-runner.js";
@@ -357,6 +358,88 @@ function publicKeyStatus(projectRoot = baseDir) {
357
358
  };
358
359
  }
359
360
 
361
+ function expandUserPath(value = "") {
362
+ const text = String(value || "").trim();
363
+ if (text === "~") return os.homedir();
364
+ if (text.startsWith("~/")) return path.join(os.homedir(), text.slice(2));
365
+ return text;
366
+ }
367
+
368
+ function uniquePaths(values = []) {
369
+ const seen = new Set();
370
+ const result = [];
371
+ for (const value of values) {
372
+ if (!value) continue;
373
+ const resolved = path.resolve(expandUserPath(value));
374
+ if (seen.has(resolved)) continue;
375
+ seen.add(resolved);
376
+ result.push(resolved);
377
+ }
378
+ return result;
379
+ }
380
+
381
+ async function readableDirectory(value = "") {
382
+ try {
383
+ const stat = await fs.stat(value);
384
+ return stat.isDirectory();
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+
390
+ async function directoryChildren(parent = "", prefix = "", limit = 20) {
391
+ const entries = [];
392
+ try {
393
+ const dirents = await fs.readdir(parent, { withFileTypes: true });
394
+ const normalizedPrefix = prefix.toLowerCase();
395
+ for (const entry of dirents) {
396
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
397
+ if (normalizedPrefix && !entry.name.toLowerCase().startsWith(normalizedPrefix)) continue;
398
+ entries.push(path.join(parent, entry.name));
399
+ if (entries.length >= limit) break;
400
+ }
401
+ } catch {
402
+ return [];
403
+ }
404
+ return entries.sort((left, right) => left.localeCompare(right));
405
+ }
406
+
407
+ async function pathSuggestions(query = "") {
408
+ const raw = String(query || "").trim();
409
+ const home = os.homedir();
410
+ const projectsLfs = path.join(home, "ProjectsLFS");
411
+ const baseCandidates = uniquePaths([
412
+ baseDir,
413
+ path.dirname(baseDir),
414
+ process.cwd(),
415
+ home,
416
+ projectsLfs,
417
+ path.join(home, "Documents"),
418
+ path.join(home, "Desktop"),
419
+ ]);
420
+ const suggestions = [];
421
+ for (const candidate of baseCandidates) {
422
+ if (await readableDirectory(candidate)) suggestions.push(candidate);
423
+ }
424
+
425
+ if (raw) {
426
+ const expanded = expandUserPath(raw);
427
+ const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(baseDir, expanded);
428
+ const parent = raw.endsWith("/") || raw.endsWith(path.sep) ? absolute : path.dirname(absolute);
429
+ const prefix = raw.endsWith("/") || raw.endsWith(path.sep) ? "" : path.basename(absolute);
430
+ suggestions.push(...(await directoryChildren(parent, prefix, 16)));
431
+ for (const root of baseCandidates.slice(0, 5)) {
432
+ suggestions.push(...(await directoryChildren(root, path.basename(expanded), 4)));
433
+ }
434
+ } else {
435
+ for (const root of baseCandidates.slice(0, 4)) {
436
+ suggestions.push(...(await directoryChildren(root, "", 4)));
437
+ }
438
+ }
439
+
440
+ return uniquePaths(suggestions).slice(0, 20);
441
+ }
442
+
360
443
  function buildRunConfig(body, overrides = {}) {
361
444
  const preferences = normalizePreferencePayload(body, db.getPreferences());
362
445
  const merged = {
@@ -841,6 +924,11 @@ app.get("/api/config", async (_req, res) => {
841
924
  });
842
925
  });
843
926
 
927
+ app.get("/api/path-suggestions", async (req, res) => {
928
+ const suggestions = await pathSuggestions(String(req.query.q || ""));
929
+ res.json({ ok: true, suggestions });
930
+ });
931
+
844
932
  app.get("/api/keys/status", (_req, res) => {
845
933
  res.json({ ok: true, keyStatus: publicKeyStatus(baseDir) });
846
934
  });