Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,22 @@ jobs:
# Kept at the same indentation as the block above: inside `run: |`, YAML strips
# only the common indentation, so a further-indented heredoc body would arrive
# in the notes with leading spaces and render as a code block.
FLAGS=""
# Both directions are stated outright rather than one of them relying on a
# default. `gh release create` defaults to "not a pre-release", so an empty
# FLAGS was correct on the create path — but the same variable is reused on the
# path below where the release already exists, and there an empty FLAGS meant a
# release someone had hand-drafted as a pre-release stayed one permanently.
# 1.0.0 is precisely the release where that would be wrong, and the sort of
# wrong nobody thinks to check for.
if [ "$PRERELEASE" = "true" ]; then
CREATE_FLAGS="--prerelease"
EDIT_FLAGS="--prerelease=true"
else
CREATE_FLAGS="--latest"
EDIT_FLAGS="--prerelease=false --latest"
fi

if [ "$PRERELEASE" = "true" ]; then
FLAGS="--prerelease"
cat >> notes.md <<'EOF'

---
Expand All @@ -161,12 +174,12 @@ jobs:
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
echo "::notice::Release ${GITHUB_REF_NAME} already exists — attaching the .vsix to it."
gh release upload "${GITHUB_REF_NAME}" "$VSIX" --clobber
if [ -n "$FLAGS" ]; then gh release edit "${GITHUB_REF_NAME}" $FLAGS; fi
gh release edit "${GITHUB_REF_NAME}" $EDIT_FLAGS
echo "::notice::Existing release notes were left unchanged. SHA-256 ${SHA256}"
else
gh release create "${GITHUB_REF_NAME}" "$VSIX" \
--title "HirayaCoder ${GITHUB_REF_NAME}" \
--notes-file notes.md \
--generate-notes \
$FLAGS
$CREATE_FLAGS
fi
87 changes: 86 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,92 @@ All notable changes to HirayaCoder are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.0] — unreleased
## [1.0.0] — 2026-08-20

The version number is the news, and it is a claim about stability rather than about
features: 1.0.0 adds no tool, no permission, no network call, and no dependency. What it
adds is the things a release has to have before strangers install it — a guide inside the
panel, a test that every control in that panel is wired to something, and a security pass
that measured what previous passes had only read.

### Added — a setup guide in the panel

A **Guide** button in the chat header opens a card covering the four setup steps and,
more usefully, what to expect once they are done: that a task takes one to five minutes
on a laptop, that a refused write is usually the checks working, and that a small model
is good at one file at a time and bad at a whole application.

The panel previously assumed its reader had read the README. For the release where
strangers arrive that is the wrong assumption: the most likely reader is someone whose
first run went nowhere because Ollama is not running, or who is four minutes into a task
and does not know whether that is normal. The card renders locally rather than asking the
host for anything — unlike every other control here, it has nothing to ask for — and sits
in the transcript rather than over it, so it can be read beside the run that prompted it.

### Fixed — 85 seconds of frozen editor, from a paste

`stepBrief.PATH_TOKEN` scans an item's text for filenames, and scanning for a token that
is not there costs one attempt per start position, so an unbroken run of word characters
was O(n²). On a single run of `a` — which is what a pasted data URI, minified line or
hash looks like to that expression — 3,200 characters took 23 ms, 51,200 took 6.1 s, and
204,800 took **85.2 seconds**, with the extension host frozen throughout.

`core/commonSense` holds the same expression and has bounded it since it was measured
there. `stepBrief` had inherited the comment explaining that a single match is linear —
which is true, and answered a different question than the one that mattered. Bounding
each segment to 120 characters returns the sweep to linear: 244 ms at 204,800 characters.
Output is unchanged on every path in this repository, and the timing is now pinned by a
test. Tracked as SAST-014.

### Fixed — a 1.0.0 could have been published as a pre-release

The release workflow already derived release-vs-pre-release from the version, so 1.0.0
was always going to come out unflagged on the normal path. The hole was the other path:
where a release for the tag already exists, the flag was only ever *added*, never
cleared, so a hand-drafted pre-release would have stayed one. Both directions are now
stated explicitly, the edit is unconditional, and full releases are marked `--latest` —
which is what `/releases/latest` resolves to, and therefore what install instructions
point at. Pinned by a test, since this is checkable exactly once per tag.

### Added — a test that every button does something

`test/unit/webviewWiring.test.js` reads `index.html`, `main.js` and `chatTab.js` as text
and checks the seams: every interactive element is resolved, every button has a way to be
activated (its own listener, a delegated container, or the form it submits), and the
message protocol closes in both directions — 13 webview-to-host types and 16 coming back.

A dead button was the one UI defect nothing here could see. The component tests build
nodes and assert their shape; the integration tests drive the host. Neither notices a
control nobody listened to, and neither looks wrong on screen until someone clicks.

### Security

Full report in `security/sast-report-2026-08-20-1.0.0.md`. Zero production dependencies,
zero advisories, no Critical or High findings.

The pass measured all 22 expressions ESLint flags as `detect-unsafe-regex` in `app/`,
timing each against an input built to be its worst case rather than reasoning about the
shape. Twenty of them are linear; one was already found and bounded; one was SAST-014
above. ESLint still reports the same 22 warnings after the fix, which is the clearest
case this codebase has for why they are reviewed rather than counted.

### Changed — the marketing set

Version badges to v1.0.0 across the six README images, plus four new 1:1 launch ads with
captions in `docs/images/ADS-1.0.0.md`. "Pre-release" is gone from the images: it
described distribution rather than stability, and a 1.0.0 calling itself a pre-release
reads as a mistake. The install route deliberately still points at GitHub Releases —
the tag is cut before the Marketplace listing exists, and `doc/PUBLISHING.md` Step 9b
says what to swap on the day it is live.

### Documentation

`doc/PUBLISHING.md` is split into a one-time first publish and the repeating update, with
a full account of what a version bump touches beyond `package.json` — ten image sources,
the README's hero alt text, the changelog, and a new SAST report, none of which fail
loudly when they go stale.

## [0.9.0] — 2026-08-20

0.8.0 gave the agent a record of what it had already done. Running the same React +
Vite + Tailwind brief again, graded this time **in a browser**, showed that the record
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
</p>

<p align="center">
<img src="https://raw.githubusercontent.com/jaymar921/HirayaCoder/main/docs/images/hero-offline-agent.png" width="900" alt="HirayaCoder v0.9.0 — your AI pair programmer, fully offline. A VS Code chat panel showing the agent reading two files, writing two files, and asking for approval before running npm run build." />
<img src="https://raw.githubusercontent.com/jaymar921/HirayaCoder/main/docs/images/hero-offline-agent.png" width="900" alt="HirayaCoder v1.0.0 — your AI pair programmer, fully offline. A VS Code chat panel showing the agent reading two files, writing two files, and asking for approval before running npm run build." />
</p>

*A local Filipino-inspired AI coder that brings imagination and speed to your VS Code workflow.*

> **Pre-release.** HirayaCoder is not on the VS Code Marketplace yet. Releases are
> published as a `.vsix` on the
> **1.0.0 is here.** Everything described below is implemented, tested on Windows, macOS
> and Linux in CI, and stable enough to put a 1.0 on. The one thing still missing is the
> one-click install: the Marketplace listing is not up yet, so releases are published as
> a `.vsix` on the
> [Releases page](https://github.com/jaymar921/HirayaCoder/releases) and installed by
> hand — [Step 4](#step-4--install-hirayacoder) has the one command it takes. Everything
> described below works today; what is missing is the one-click install.
> hand — [Step 4](#step-4--install-hirayacoder) has the one command it takes.

**HirayaCoder is a free AI coding assistant that runs entirely on your own computer.**
You type what you want in plain English, and it writes and edits the files for you — no
Expand Down Expand Up @@ -132,6 +133,11 @@ Good first things to type:
- `create a simple to-do list app in one HTML file`
- `explain what this project does` *(in a folder that already has code)*

**If anything above is unclear once you are in there, press *Guide* in the chat header.**
It opens the same four setup steps and — more usefully — what to expect: how long a task
takes, why a refusal is usually the checks working, and what a small model is and is not
good at.

There is a longer, friendlier walkthrough in
[TUTORIAL.md](https://github.com/jaymar921/HirayaCoder/blob/main/doc/TUTORIAL.md).

Expand Down
20 changes: 17 additions & 3 deletions app/agent/stepBrief.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,24 @@ const MAX_CONSTRAINT_CHARS = 500;
* A path, or a path-like token, inside an item's text.
*
* The repeated group is separated by a mandatory `/`, which the character class
* excludes, so no two iterations can claim the same characters and the match is linear
* despite the nested quantifier the linter flags.
* excludes, so no two iterations can claim the same characters and *one* match is
* linear despite the nested quantifier the linter flags.
*
* The cost that is not linear is scanning with `/g` for matches that are not there.
* Every start position inside an unbroken run of word characters gets its own scan,
* which makes the sweep O(n²) in the length of that run — the same shape
* `core/commonSense` documents and bounds, in the module that copied its comment and
* not its bound. Measured on a single run of `a`, which is what a pasted data URI,
* minified line or hash looks like to this expression: 23 ms at 3,200 characters,
* 6.1 s at 51,200, and **85 s at 204,800**. That is a hard freeze of the extension
* host, reached by a paste rather than by an attack.
*
* Bounding the segment is what fixes it: work per start position is capped, so the
* sweep is linear again — 244 ms at 204,800 characters. 120 is far past any real path
* segment, and the filter below still requires an extension or a `/`, so nothing this
* repo has ever matched is affected. Pinned by a test rather than left to the comment.
*/
const PATH_TOKEN = /[\[email protected]]+(?:\/[\[email protected]]+)+|[\w@-]+\.[a-z0-9]{1,6}\b/gi;
const PATH_TOKEN = /[\[email protected]]{1,120}(?:\/[\[email protected]]{1,120})+|[\w@-]{1,120}\.[a-z0-9]{1,6}\b/gi;

/**
* The files an item names outright.
Expand Down
181 changes: 181 additions & 0 deletions app/webview/components/guideCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* The setup guide — what to install, and what the thing actually does once installed.
*
* ## Why this is static text in the webview
*
* Every other control here posts to the host, because the host is the only side that
* can do anything. This one has nothing to ask for: the guide is the same sentences on
* every machine, in every workspace, whether or not Ollama is running. Routing it
* through a message would buy a protocol and a round-trip for a string constant.
*
* ## Why it is a card in the transcript rather than a modal
*
* The most likely reader is someone whose first run did not go the way they expected —
* a model that has not been pulled, a task that is taking four minutes, a write that
* was refused. They need the guide *beside* the thing that confused them, and they need
* to keep scrolling back to it. A modal takes the transcript away to show it.
*
* The content is deliberately blunt about the trade. A first-time user who is told to
* expect ChatGPT and gets a 1B model will conclude the extension is broken; one who is
* told a task takes one to five minutes on a laptop will wait for it.
*
* @module webview/components/guideCard
*/

/**
* @typedef {object} GuideStep
* @property {string} title
* @property {string} detail
* @property {string} [command] A line to paste into a terminal, if the step has one.
*/

/** @type {GuideStep[]} */
const SETUP = [
{
title: 'Install Ollama and leave it running',
detail:
'Ollama is the free program that runs the AI on your own machine. Download it from ollama.com. It has no window — it sits in your system tray or menu bar, and that is normal.',
},
{
title: 'Download one model',
detail:
'Paste this into a terminal. It downloads a few gigabytes once, then never again. On 8 GB of RAM use llama3.2:1b instead.',
command: 'ollama pull gemma4:e2b',
},
{
title: 'Open a folder',
detail:
'File → Open Folder. This is required, not a suggestion: HirayaCoder confines every file operation to the folder you opened, so with no folder open there is nowhere it is allowed to work.',
},
{
title: 'Pick your model above and start typing',
detail:
'The dropdown in this header lists what Ollama has installed. Ask for one thing at a time — "add a delete button to index.html" goes much better than "build me a social network".',
},
];

/** @type {Array<{title: string, detail: string}>} */
const EXPECT = [
{
title: 'It is slower than you are used to',
detail:
'A task takes 1–5 minutes on a laptop with no graphics card, 20–60 seconds with one. The step panel shows you each action as it happens so you can tell "thinking" from "stuck" — and stop it when it is the second one.',
},
{
title: 'Nothing is saved until you approve it',
detail:
'Every write shows you a diff first. Turn on Auto Edit from the Permissions button once you trust it; deleting a file asks even then.',
},
{
title: 'A refusal is usually the checks working',
detail:
'Writes that would truncate a file, drop an export, or leave a stub inside a function are blocked before they reach the disk. Ask again — it usually gets it right the second time.',
},
{
title: 'Small models are capable, not clever',
detail:
'One file, one feature, one fix at a time is where a local model is genuinely good. Handed a whole application it will write plausible files that do not run together. That is a real limit, not a setting you have missed.',
},
{
title: 'Three modes, and Agent is the right default',
detail:
'Agent reads and writes. Plan looks without touching anything and hands back a checklist you can edit and then run. Ask answers a question with no tools at all. You do not need to switch to Ask to ask something — Agent notices a question and just answers it.',
},
];

/**
* One titled paragraph, optionally with a copyable-looking command under it.
*
* @param {string} tag The element for the title — `li` items carry their own marker.
* @param {{title: string, detail: string, command?: string}} entry
* @returns {HTMLElement}
*/
function renderEntry(tag, entry) {
const item = document.createElement(tag);
item.className = 'guide-item';

const title = document.createElement('span');
title.className = 'guide-item-title';
title.textContent = entry.title;
item.appendChild(title);

const detail = document.createElement('span');
detail.className = 'guide-item-detail';
detail.textContent = entry.detail;
item.appendChild(detail);

if (entry.command) {
const command = document.createElement('code');
command.className = 'guide-command';
command.textContent = entry.command;
item.appendChild(command);
}

return item;
}

/**
* @param {string} heading
* @param {string} listTag `ol` for the ordered setup steps, `ul` for the rest.
* @param {Array<{title: string, detail: string, command?: string}>} entries
* @returns {DocumentFragment}
*/
function renderSection(heading, listTag, entries) {
const fragment = document.createDocumentFragment();

const title = document.createElement('h3');
title.className = 'guide-heading';
title.textContent = heading;
fragment.appendChild(title);

const list = document.createElement(listTag);
list.className = 'guide-list';
for (const entry of entries) list.appendChild(renderEntry('li', entry));
fragment.appendChild(list);

return fragment;
}

/**
* Build the guide card.
*
* @param {() => void} onDismiss Called when the reader closes it.
* @returns {HTMLElement}
*/
export function renderGuide(onDismiss) {
const wrapper = document.createElement('section');
wrapper.className = 'guide';
wrapper.setAttribute('aria-label', 'Setup guide');

const bar = document.createElement('div');
bar.className = 'guide-bar';

const title = document.createElement('h2');
title.className = 'guide-title';
title.textContent = 'Setting up, and what to expect';
bar.appendChild(title);

const close = document.createElement('button');
close.className = 'chip-remove';
close.type = 'button';
close.textContent = '×';
close.setAttribute('aria-label', 'Close the guide');
close.addEventListener('click', () => onDismiss());
bar.appendChild(close);

wrapper.appendChild(bar);

const blurb = document.createElement('p');
blurb.className = 'guide-blurb';
blurb.textContent =
'Everything runs on your machine. No account, no internet after setup, and nothing you type or open leaves this computer.';
wrapper.appendChild(blurb);

wrapper.appendChild(renderSection('Setup — four steps', 'ol', SETUP));
wrapper.appendChild(renderSection('What to expect', 'ul', EXPECT));

return wrapper;
}

/** Exported for the tests, which assert the guide covers each of these. */
export const sections = { SETUP, EXPECT };
Loading
Loading