Skip to content

v3.6.0 into main - #113

Merged
vinnybod merged 19 commits into
mainfrom
release/3.6.0
Aug 1, 2026
Merged

v3.6.0 into main#113
vinnybod merged 19 commits into
mainfrom
release/3.6.0

Conversation

@vinnybod

@vinnybod vinnybod commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

This PR was automatically generated by the release-public-start workflow.
This PR should be merged with a merge commit, not a squash commit.
Merging this PR will trigger a tag and release automatically.

vinnybod and others added 19 commits May 2, 2026 11:28
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](actions/cache@v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* Convert global mixins to composables

Replace the two global Options API mixins with Vue 3 composables under a
new src/composables/ directory:

- useDownload() -> { downloadStager, downloadText } (stateless DOM/blob
  helpers, logic relocated from the download-stager mixin)
- useCopyStager() -> { copyStager }, injecting "snack" internally and
  delegating to copyToClipboard() exactly as before

Migrate all 8 consumers off the mixins via a minimal setup() bridge that
re-exposes the same method names the templates already use, leaving the
Options API components otherwise unchanged. Delete the now-empty
src/mixins/ directory.

Add an .eslintrc.js override disabling import/prefer-default-export for
src/composables/** so composables can follow the idiomatic named-export
(useFoo) convention.

Tighten JSDoc on the composables ({string} params, document the
empty/trailing-slash no-op in downloadStager) and drop a redundant
always-true guard in downloadStager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for mixin-to-composable conversion

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Replace moment with dayjs

moment is deprecated. dayjs is a near drop-in replacement with identical
format tokens, so every existing format string is unchanged byte-for-byte.

- Add src/plugins/dayjs.js: a central dayjs instance with the relativeTime
  plugin extended (required for .fromNow()). All consumers import the
  configured instance from "@/plugins/dayjs".
- Swap moment for dayjs in DateTimeDisplay, ClickToEdit, AgentJobs, and
  AgentForm (import, data() registration, and call sites in script +
  template). Format strings and fromNow() behavior are unchanged.
- package.json: drop "moment", add "dayjs" ^1.11.20.

Verified: dayjs fromNow() phrasing matches moment's ("a few seconds ago",
"2 minutes ago", "in 5 minutes"); all format outputs identical. Lint clean
(0 errors), prettier clean, yarn build succeeds, and the agent-detail /
agent-jobs / agents-list e2e specs pass (6/6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop unused dayjs registration from ClickToEdit data()

ClickToEdit registered dayjs in data() but never used it: the template
references no date helper, and the only call site (onDatePicked) uses the
module-level imported dayjs, not this.dayjs. This was dead with moment too
and got faithfully ported in the migration; remove it now. Behavior-neutral.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Guard fromNow()/format() against invalid timestamps

Round-2 review caught a behavioral divergence: moment(null) / moment("")
rendered "Invalid date", but dayjs treats those as valid and emits a
confidently-wrong relative time ("a month ago"). Reachable via the
checkin_time/lastseen_time fields in AgentForm and the timestamp prop in
DateTimeDisplay (used across ~9 tables) if the backend ever sends null/"".

Move the inline template dayjs() expressions into a computed (DateTimeDisplay)
and a method (AgentForm) that short-circuit on dayjs().isValid(), rendering
the existing "N/A" placeholder (matches AgentJobs.formatDate) instead of a
bogus value. dayjs(undefined) stays valid (= now), so fields the backend
omits render exactly as before — identical to moment, and e2e stays green.

Moving the expressions out of the templates also makes the dayjs data()
registrations dead, so drop them (same cleanup as the ClickToEdit commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for moment-to-dayjs migration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…table keys) (#283)

* Remove dead vue.config.js

Leftover vue-cli config that Vite ignores. Nothing in the repo
imports or references it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add jsconfig.json for "@/" alias resolution

Lets editors resolve the "@/*" -> "./src/*" alias used throughout the
codebase. Editor-only metadata; does not affect the Vite build or ESLint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Declare Node >=20 in package.json engines

CI already pins Node 20; declare it explicitly so local environments
and tooling match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove leftover debug logging

Drop console.log/console.warn debug noise in:
- stores/listener-module.js (autorun save confirmation)
- api/agent-api.js (killAgent args)
- api/listener-api.js (raw error before reject)
- api/download-api.js (response headers / filename)

Error handling (handleError + reject, console.error) is left intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove unused uuid dependency

uuid had zero references anywhere in src/ — it was a dead dependency.
Drop it from package.json and the lockfile rather than upgrading it.
No transitive dependency requires it. If random ids are needed later,
the built-in crypto.randomUUID() (Web Crypto / Node >=20) covers the
v4 case without a dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Declare emits on components that were missing it

Add explicit emits: options to components that emit events without
declaring them, satisfying vue/require-explicit-emits:
- TagChip (delete-tag, update-tag)
- TagViewer (delete-tag, update-tag, new-tag)
- HeaderMenu (submit)
- TooltipButton (click)
- PluginTasksTable / AgentTasksTable (refresh-tags)
- AgentFileBrowser (openUploadDialog)
- AgentExecuteModule (moduleChange, submitted)
- AgentForm (refresh-agent)
- ModulesTable (languages-changed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use stable v-for keys for mutable lists

Replace array-index keys on lists that reorder with stable ids:
- Chat.vue: assign an incrementing id to each pushed message and key
  on it (via a small addMessage helper) instead of the loop index.
- Notifications: addNotification now stamps each notification with a
  unique id (timestamp + counter, stable across reloads since
  notifications are persisted). NotificationBell and the Notifications
  view key on item.id. Notifications are prepended, so index keys were
  genuinely unstable. afterRestore backfills ids on notifications that
  were persisted before ids existed, so they don't all key on undefined.

Static / append-only lists (terminal output, author chips, info
comments, common stagers, obfuscation configs, header menu) were left
on index keys intentionally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entries for Vue quick-win modernization

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Migrate ESLint to v9 flat config on the official Vue stack

Drop the legacy eslintrc + airbnb stack in favor of ESLint 9 flat config on
the official Vue baseline: eslint-plugin-vue (flat preset) +
@vue/eslint-config-prettier (skip-formatting) + Prettier as its own step.
This is a tooling-only change — no source files were reformatted and no app
behavior changed.

Deps:
- eslint ^8 -> ^9; add @eslint/js, globals, @eslint/compat.
- Bump eslint-plugin-vue to ^9.33 (exposes flat configs).
- Replace eslint-config-prettier with @vue/eslint-config-prettier ^10
  (skip-formatting export: disables stylistic rules, keeps prettier/prettier
  OFF so Prettier still runs separately via `format` / `format:check`).
- Remove @vue/eslint-config-airbnb and @rushstack/eslint-patch.

Config (eslint.config.js): @eslint/js recommended, eslint-plugin-vue
flat/recommended, @VUE skip-formatting, then a project block (applied last, so
its overrides win over the presets/prettier disables — matching the precedence
the old eslintrc `rules:` block had over `extends`) with browser globals and
all preserved overrides ported verbatim. .gitignore is honored via
includeIgnoreFile (replaces --ignore-path); dist/dist_electron ignored
explicitly. Node globals applied to *.config.js + e2e/**; vue.config.js linted
as CommonJS. The old mocha __tests__ override is dropped (no unit tests; tests
are Playwright e2e).

import/no-unresolved decision — DROPPED (not enforced). The Vite build already
fails on unresolved imports, so enforcing it in ESLint would mean maintaining a
separate resolver for the "@" -> ./src alias for no added safety. The whole
eslint-plugin-import dependency is dropped (also drops import/no-cycle and the
src/plugins/vuetify.js override, which are only relevant with an import plugin).
Because the source carries pre-existing inline `// eslint-disable import/...`
comments and editing source is out of scope here, a tiny no-op stub registers
those rule names so the directives resolve (flat config errors on directives
naming undefined rules). Using eslint-plugin-import-x instead would drag in
@typescript-eslint/utils — unwanted in a no-TypeScript project.

Rule relaxers added to preserve parity (not airbnb rules — ESLint 9 default
changes / flat-config defaults):
- no-unused-vars caughtErrors: "none" — ESLint 9 flipped the default to "all",
  newly flagging 3 pre-existing `catch (err)` bindings. Pinned back to the
  ESLint 8 behavior to avoid editing source.
- linterOptions.reportUnusedDisableDirectives: "off" — flat config defaults
  this to "warn"; off matches the eslintrc-era default and prevents the
  vestigial inline disable comments (for now-unconfigured airbnb/core rules)
  from warning or being auto-stripped by `--fix`.

Lint script -> "eslint --fix ." (drops invalid --ext / --ignore-path flags);
format / format:check untouched.

Verified: `yarn lint` clean (0 errors, 31 warnings — all pre-existing
vue/no-v-html + vue/require-explicit-emits, same as before); `yarn build`
succeeds; `yarn format:check` clean (no formatting diff); --print-config
confirms vue rules + browser globals + overrides resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for ESLint flat-config migration

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Extract ANSI helpers and auto-refresh polling into shared code

Pure refactor; behavior, polling cadence, and rendered output unchanged.

Part A — ANSI helpers → src/utils/ansi.js
- New util exports stripAnsi/isAnsi/ansiToHtml (moved verbatim, same regex
  and per-call `new AnsiUp()` semantics).
- AgentTasksTable, PluginTasksTable, AgentTerminal, AgentShellSession now
  import from the util; the two terminals keep their `ansiToHTML` template
  name via an `ansiToHTML: ansiToHtml` method alias.
- AgentJobs intentionally reuses a single stateful AnsiUp instance across
  calls, so the util re-exports the AnsiUp class and AgentJobs keeps its
  module-level instance (only the import source changed) to preserve output.
- No `ansi_up` import remains outside src/utils/ansi.js.

Part B — auto-refresh polling → src/composables/useAutoRefresh.js
- New composable owns the interval lifecycle and clears it on
  onBeforeUnmount, exposing { autoRefresh, start, stop }.
- AgentTasksTable, PluginTasksTable, and AgentsTable wire it via a minimal
  setup() (getCurrentInstance().proxy bridges the Options API callbacks) and
  drive start()/stop() synchronously from their existing prop watchers, so
  each component keeps its exact interval (8000ms), fetch callback
  (debouncedGetTasks vs getAgents), and immediate-fetch timing.
- Removed the per-component refreshInterval data field, inline setInterval,
  and beforeUnmount clearInterval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for shared ANSI/auto-refresh extraction

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add Vitest unit-test harness and first batch of data-layer tests

Stand up a Vitest unit-testing setup alongside the existing Playwright e2e
suite (which is untouched) and seed it with high-signal tests for pure data
-layer logic.

- deps: vitest@3.2.4, @vue/test-utils@2.4.10, @pinia/testing@0.1.7,
  happy-dom@20.9.0 (pinned for Vite 5 / Vue 3.5 / Pinia 2 compatibility)
- vite.config.js: switch to defineConfig from vitest/config; add test block
  (globals, happy-dom env) scoped to src/ so Vitest stays out of e2e/
- package.json: add test:unit (watch) and test:unit:run (one-shot) scripts
- .eslintrc.js: replace the stale mocha override with Vitest globals so
  test files lint cleanly
- tests: listener-module (addListener dedup, removeListener, getListeners
  status flow, listenerNames/templateIds getters), bypass-module
  (mergedBypassNames merge/dedup), and utils/tags fetchDedupedTags

15 tests, all passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address first-pass review: cover error branches, tighten eslint glob

Review feedback from the pr-review-toolkit pass:
- listener-module.spec: add tests for killListener (api call + state removal),
  fetchAutorunTasks (success extraction + reject-returns-[] / state-intact),
  and saveAutorunTasks (records-wrapping payload shape + error swallowing) —
  these were the highest-signal untested branches. Silence console in
  beforeEach so the error-path tests don't pollute output.
- bypass-module.spec: add vi.clearAllMocks() in beforeEach for consistency.
- .eslintrc.js: scope the Vitest globals override to src/ so it no longer
  matches the e2e/ Playwright specs; refine the mock comment.

20 unit tests, all passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for Vitest unit-test harness

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Trim unused test deps; use node Vitest environment

No unit test touches the DOM or mounts a component, so happy-dom and
@vue/test-utils are unused. Drop them and run under the default node
environment; re-add them with the first component test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix getListeners leaving status stuck on "loading" when fetch rejects

A rejected listeners fetch previously left status at "loading" forever
(hanging spinner, no error surfaced). Wrap in try/catch matching the
agent-module convention (log + status="error"), and add a reject-path test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…4, Vitest 4 (#289)

Moves the framework stack forward to the latest majors, removing the Vite 5 /
Pinia 2 constraint that pinned the Vitest harness (PR #282) to vitest@3.2.4 /
@pinia/testing@0.1.7.

- Vite 5 -> 8 (Rolldown/Oxc) and @vitejs/plugin-vue 5 -> 6
- Pinia 2 -> 3 and pinia-plugin-persistedstate 3 -> 4
- Vitest 3 -> 4 and @pinia/testing 0.1 -> 1
- vite-plugin-vuetify, Vuetify, and vue-router left untouched (work as-is)

persistedstate v4 changes handled:
- Renamed the application-module persist hook afterRestore -> afterHydrate
  (v4 rename); it still re-applies the axios instance url/token after rehydrate.
  Wrapped the setInstance call in try/catch + an explicit log, since v4 swallows
  hook errors when debug is off and this hook is the only path that rebuilds the
  axios instance after a reload.
- chatUnreadCount is now correctly omitted: v4 honors `omit`, which v3 silently
  ignored, so the field was previously being persisted.

CI: bumped all workflows from Node 20 to Node 22 to meet Vite 8's Node baseline
(^20.19.0 || >=22.12.0).

Verified: yarn install (clean), test:unit:run (20/20), build (ok), e2e (40/40
serial, matching CI's workers=1), lint (31 warnings/0 errors, unchanged from
base), and a direct persistedstate v4 localStorage round-trip check.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…able (#287)

* Fix socket handler + timer leaks in Chat via useSocketHandlers composable

Chat.vue is a v-if'd consumer of the app's single socket (owned by
SocketNotifications). On each mount it registered chat/join, chat/leave,
chat/message, and chat/participants listeners with anonymous callbacks but
unmounted() only emitted chat/leave — it never removed them. Because the
socket outlives Chat, toggling the chat widget setting accumulated duplicate
handlers, causing duplicate message processing and inflated unread counts.
mounted() also started a 1s setTimeout with no clearTimeout on unmount.

- Add src/composables/useSocketHandlers.js: useSocketHandlers(socket) returns
  on(event, handler) (records each pair) and emit(...) passthrough, and via
  onUnmounted calls socket.off for every registered handler. It manages
  HANDLER lifecycle only — never opens or closes the socket.
- Chat.vue: add a minimal setup(props) that bridges the composable's on/emit
  into the Options API instance; route the four chat/* registrations through
  on() so they auto-remove on unmount. Capture the history timer id and
  clearTimeout it in unmounted(). Handler logic (unread counting, historyLoaded
  gating, skipping own messages) is unchanged; template is unchanged.
- SocketNotifications.vue: remove the "Opening Socket"/"Closing Socket" debug
  logs. Did NOT adopt the composable here — its setAgentHandlers does
  conditional socket.off and re-registers on subscription changes, which
  doesn't fit the record-and-cleanup model, and those handlers are torn down
  with the socket anyway (no leak). Connection creation/teardown untouched.

Chose the setup() bridge over component-level bookkeeping because the shared
cleanup logic must live in the composable, and onUnmounted only fires from
setup; the socket prop is stable (Chat fully remounts when toggled) so the
one root-scope prop read is intentional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add CHANGELOG entry for chat socket handler leak fix

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Document useSocketHandlers @throws contract in JSDoc

The composable fails fast when no socket is provided; note that in the
JSDoc so callers know the contract is enforced at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add design spec: replace axios with native fetch

Records the decided HTTP-layer direction for Starkiller: remove axios
(^0.24.0) and qs in favor of native fetch behind a centralized
src/api/http.js wrapper, with a Vitest contract suite built first and a
phased migration across the 13 api modules + 4 bare login calls.

Investigation/design only — no implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revise HTTP fetch spec per adversarial review

Addresses 3 critical + 3 medium + 1 low finding:
- Normalize leading-slash-free paths in request() (18 call sites)
- Axios-compatible error shape (err.response.{status,statusText,data})
  so handleError, extractErrorMessage, and direct readers (ListenerEdit)
  keep working unchanged
- Forced exception #2: functional-form calls return parsed body; their
  consumers (FileUploadDialog/Downloads/Settings) updated
- safeParse no longer trusts content-length; defensive try/catch parse
- Differential qs-parity test on the real getTasks param shape
- Spell out all 3 blob/text download paths + triggerDownload helper
- Step 0 blast-radius grep; step 1 rewires setInstance to http.js
- Note module-api consumer (AutoRunModules) tolerates new handleError

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revise HTTP fetch spec per adversarial re-review (round 2)

Addresses 1 critical + 2 medium + 1 low:
- toQuery normalizes "+" -> "%20" so query bytes stay identical to qs
  (URLSearchParams encodes spaces as "+"); keeps the differential test valid
- §7 test must include a free-text query value with a space
- Login/refreshMe stay on raw fetch in the store (NOT request()), preserving
  interceptor-free behavior; drop the auth flag from the wrapper entirely
- Correct forced-exception-#2 consumer list: only FileUploadDialog reads .data;
  Downloads.vue/Settings.vue verified no-ops
- Document accepted divergence: malformed 2xx body -> undefined (vs axios string)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fold in round-3 advisory findings (gate passed)

Adversarial review round 3 verdict: APPROVE (0 critical). Advisory:
- Raw-fetch login must check !res.ok before reading the body (it bypasses
  request()'s centralized throw); spec now mandates a rawFetchJson() helper
  so a wrong-password 401 can't slip a bad token through
- Restore Pragma/Expires cache headers in the getDownload example

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add implementation plan: replace axios with native fetch

9-task TDD plan: Vitest infra, the http.js wrapper + contract suite,
dual-init wiring, raw-fetch login, staged migration of all 13 api modules,
and cleanup removing axios + qs. Each commit keeps the app working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: add Vitest unit-test infrastructure

* feat: add native-fetch http wrapper with contract tests

* refactor: initialize http wrapper from setInstance during migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: migrate login and refreshMe to raw fetch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: migrate tag-api to fetch wrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: migrate credential/plugin/agent-task apis to fetch wrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: migrate download-api to fetch wrapper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: migrate remaining api modules to fetch wrapper

* refactor: remove axios and qs dependencies

Completes the axios→native-fetch migration. Deletes src/api/axios-instance.js,
switches the store's setInstance import to @/api/http, removes the temporary
qs differential test from http.spec.js, and runs `yarn remove axios qs`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address review findings (download error handling, error-body guard, tests, stale comments)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: simplify fetch migration code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Remove superpowers planning docs and add CHANGELOG entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply Prettier formatting to the new fetch wrapper + migrated modules

CI's lint job runs yarn format:check (Prettier --check) in addition to
ESLint; that step was failing on the new files because I only ran eslint
locally. Pure whitespace — Prettier wrapping long lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address PR feedback: shorthand methods, harden safeParse, route refreshMe through wrapper

- Add request.get/post/put/delete shorthand to http.js and migrate the 13
  api/*.js modules to use it (~75 call sites). Restores the call-site
  ergonomics axios had before this migration.
- Make safeParse distinguish empty body (return undefined for 204/DELETE)
  from a non-empty body that fails to parse — throw an axios-shaped error
  so a reverse-proxy 200-HTML page or truncated response surfaces via the
  existing .catch(handleError) chain instead of crashing a list consumer
  with TypeError: undefined.records.
- Route refreshMe through request.get() so a 401 triggers logout-on-auth
  like every other authenticated endpoint, and await it at Settings.vue so
  failures surface in the surrounding try/catch instead of dropping as an
  unhandled rejection. Only login retains the raw-fetch bypass — that's
  the case where logout-on-401 is actively wrong.
- Add a setInstance preflight check so calling request() before the API
  client is initialized throws a clear error instead of looking like a
  network outage via the catch-all connectionError++ path.

Tests:
- New application-module.spec.js pins the login bypass invariant: 401 and
  network-failure on login must NOT trigger logout() or bump
  connectionError. The whole point of the bypass; previously asserted by
  nothing.
- Refactor the blob test to assert the .headers.get()/.blob() contract
  that download-api actually consumes, not identity with the mock object.
- Add tests for skipNulls falsy-value semantics (0/false/"" survive),
  caller-provided headers merging with auth, and Content-Type override
  behavior.
- Add shorthand-method contract tests (delegates to GET/POST/PUT/DELETE,
  no-body POST, FormData passthrough, params forwarding).
- Update the "non-JSON 200 body" test to reflect the safeParse fix.

CHANGELOG: Fixed entry for FileInput auto-select side-effect (createDownload
previously returned the response wrapper, so .id was undefined).

51/51 unit tests pass; lint clean; prettier clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop semver dep in favor of inline version comparator

Only two call sites in App.vue, both using semver.satisfies() with
trivial single-comparator ranges ("<5.2", ">=4.0"). The full semver
package is ~260KB and is one of the surfaces flagged in our dependency
audit; this commit replaces it with a ~30-line satisfies() that handles
the operators we actually use (>=, <=, >, <, =) and matches semver's
behavior of returning false for non-numeric/invalid versions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop table dep in favor of inline ASCII table renderer

Two call sites in AgentTerminal.vue, both rendering simple bordered
ASCII tables for the terminal view. The `table` package (3.1 MB +
transitive ajv/lodash.truncate/slice-ansi/string-width/strip-ansi) is
flagged in our dependency audit. This commit replaces it with an
~80-line renderer matching the package's output byte-for-byte for the
two configs we use (vetted via Vitest goldens captured against the
upstream package before removal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address PR-review feedback: harden inline helpers, add CHANGELOG entry

CHANGELOG
- Added Removed entries for `semver` and `table` under [Unreleased]
  alongside the existing axios/qs removal entry.

version.js
- Strip a leading "v" in parseVersion so "v5.2.0" parses the same as
  "5.2.0", matching semver.satisfies's tolerance. Without this, a stray
  "v" in Empire's version response would silently kill the >=4.0
  socket-panel gate and the <5.2 compatibility warning at App.vue. The
  endpoint is contractually plain dotted, but defensive parity is cheap.
- Updated spec: dedicated v-prefix block asserting the new behavior.

ascii-table.js
- Honor embedded "\n" in cells as forced line breaks (matches the
  upstream `table` package). Without this, a literal newline in a
  module description visually shattered the table.
- Auto-size columns to the widest sub-line (not the raw string length)
  so a "one\ntwo" cell sizes as 3, not 7.
- Guard `wrapToWidth(_, 0)` — without it, `i += 0` spins forever and
  hangs the browser tab. Reachable for any future caller that sets
  `columns: { N: { width: 0 } }`.
- Throw on row-length mismatch instead of silently truncating extras /
  padding missing cells. Restores the loud-failure contract `table`
  provided.
- Warn on non-primitive cells instead of silently rendering
  "[object Object]" — Empire's contract is strings/booleans, so a
  regression now surfaces in the console.
- Extracted BOX_BORDER constant exported from ascii-table.js and
  imported by both AgentTerminal.vue and the spec, so the byte-for-byte
  parity test can't pass against a stale local copy while production
  renders differently.
- Spec: pinned the upstream-capture version (`table@^6.8.1`) in the
  goldens comment so future recaptures are deterministic. New tests for
  each safeguard above.

* Second-pass review fixes: case-insensitive v-strip + freeze BOX_BORDER

version.js
- parseVersion now strips a leading "v" OR "V" (was lowercase-only).
  node-semver itself uses /^[vV]/, so the comment claiming "matches
  semver tolerance" was aspirational. Closes the same silent-gate
  failure mode the original v-strip addressed, but for capital V.
- Spec: added "V5.2.0" to the v-prefix block.

ascii-table.js
- Object.freeze(BOX_BORDER). Vue puts the imported singleton inside a
  reactive proxy in AgentTerminal.vue's data(); a stray mutation via
  `this.tableConfig.border.X = ...` would proxy-trap through and
  poison every subsequent render across all consumers. With freeze,
  that mutation now throws loudly under strict mode (which Vue
  components run under) instead of silently corrupting state.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the server-side validation feedback / GeneralForm refactor from #290,
which had no changelog entry, and compress the Unreleased section to the
terse one-line-per-change style used in 3.2 and earlier releases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vinnybod
vinnybod merged commit b7c4db9 into main Aug 1, 2026
2 checks passed
@vinnybod
vinnybod deleted the release/3.6.0 branch August 1, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants