Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a7621d7
fix(terminal): a burst of grid changes reaches the pty as at most one…
chaitanyagiri Oct 19, 2026
120e231
fix(theme): dropdowns read in the dark theme
chaitanyagiri Oct 19, 2026
b8549a5
fix(floor): an agent rests a few minutes after a coffee break
chaitanyagiri Oct 19, 2026
b7cd1c3
feat(app): a render error stays in its place instead of blanking the …
chaitanyagiri Oct 19, 2026
6b7270a
fix(worktree): never delete through a link when removing a worktree
chaitanyagiri Oct 19, 2026
a0e4437
fix(hive): keep a damaged config, agent list or board instead of repl…
chaitanyagiri Oct 19, 2026
2f9b8af
perf(codex): a hive Codex agent runs with plugins off and tool output…
chaitanyagiri Oct 19, 2026
275b1fc
feat(graphics): a Graphics mode (Auto, GPU, CPU only), main side
chaitanyagiri Oct 19, 2026
ce0f4ab
feat(theme): accept the 'starhike' office theme id
chaitanyagiri Oct 19, 2026
435d516
feat(history): read an agent's conversation from its CLI's own transc…
chaitanyagiri Oct 19, 2026
4135b33
fix(codex): a plugin's sub-table is not a plugin
chaitanyagiri Oct 19, 2026
ba4d0f5
feat(floor): stations floor engine and the Star Hike pack, renderer only
chaitanyagiri Oct 19, 2026
7db1fdb
feat(theme): accept the 'starhike' office theme id
chaitanyagiri Oct 19, 2026
7f71e80
feat(askme): main writes one question's answer, with options
chaitanyagiri Oct 19, 2026
df49475
feat(theme): each project keeps its own floor theme
chaitanyagiri Oct 19, 2026
387afc0
feat(window): a view that dies is reloaded, and says it was restored
chaitanyagiri Oct 19, 2026
d864806
feat(codex): hold a Codex start until its folder's .codex is allowed
chaitanyagiri Oct 19, 2026
8ea5878
feat(codex): never type into a Codex screen that is not the composer
chaitanyagiri Oct 19, 2026
db6b2f0
feat(theme): the window's config type accepts 'starhike'
chaitanyagiri Oct 19, 2026
b72c2f4
fix(theme): a pack's orchestrator seed may have no name
chaitanyagiri Oct 19, 2026
d8a31e2
feat(theme): the window's config type accepts 'starhike'
chaitanyagiri Oct 19, 2026
df082a1
fix(codex): hold on a /side conversation and on the setup import screen
chaitanyagiri Oct 19, 2026
316822e
feat(floor): Star Hike in the floor picker, with its credit
chaitanyagiri Oct 19, 2026
fa09790
feat(agents): a look per pack floor in Add Agent and Edit Agent
chaitanyagiri Oct 19, 2026
ea8d156
feat(agents): the pack look on agent cards, and the orchestrator's fi…
chaitanyagiri Oct 19, 2026
1f15181
fix(codex): hold on agents/ and hooks/, on every key that is not conf…
chaitanyagiri Oct 19, 2026
84ee040
Merge comm/m30-170-ui (ea8d156c) into comm/m30-170-main
chaitanyagiri Oct 19, 2026
1a7010b
feat(theme): main registers the bundled theme packs by the window's o…
chaitanyagiri Oct 19, 2026
1ceafce
fix(codex): a held start's take back deletes the branch made for it w…
chaitanyagiri Oct 19, 2026
b7ea79d
feat(theme): a new project's home change carries the floor chosen for it
chaitanyagiri Oct 19, 2026
583f2e2
feat(theme): each project picks its own floor in Settings, and a swit…
chaitanyagiri Oct 19, 2026
c7f3cf3
feat(ui): pick the floor when a new project starts
chaitanyagiri Oct 19, 2026
31d731a
feat(ui): ask before a Codex agent runs a folder's own settings
chaitanyagiri Oct 19, 2026
0a9abbf
feat(ui): Ask Me shows the options a question offers
chaitanyagiri Oct 19, 2026
bc19757
feat(ui): say when the window came back and when a file is paused
chaitanyagiri Oct 19, 2026
9bd4f1a
fix(ui): Ask Me text follows its own direction only in an Arabic window
chaitanyagiri Oct 19, 2026
d0a27be
feat(ui): choose how the app draws, in Settings
chaitanyagiri Oct 19, 2026
05ba84c
feat(history): a History tab in the agent panel
chaitanyagiri Oct 19, 2026
dc98758
fix(i18n): graphics.confirm_one in every locale, with the number writ…
chaitanyagiri Oct 19, 2026
bbd57da
test: the Star Hike floor, each project's floor, the Codex folder hol…
chaitanyagiri Oct 19, 2026
20e8642
test: removing a worktree reports the dependency link it took out first
chaitanyagiri Oct 19, 2026
68ac6b5
fix(portraits): every agent portrait wears its floor look
chaitanyagiri Oct 19, 2026
560a1b3
test: every portrait of an agent draws its floor look, not only the card
chaitanyagiri Oct 19, 2026
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
7 changes: 7 additions & 0 deletions LICENSE-ASSETS
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ The Office cast sprites are not LimeZu art -- they are drawn procedurally in
src/renderer/src/scene/office/portraitArt.ts and are covered by the MIT License
like the rest of the source.

The Star Hike floor art under themes/starhike/ (art/ and map/) is not LimeZu
art and not MIT either. It is "Artwork by Prozzta" (https://github.com/Prozzta/guppyos),
licensed under themes/starhike/LICENSE-ARTWORK.md, which permits editing and use
in commercial and non-commercial projects, does not permit reselling or
distributing the art itself, and REQUIRES the credit "Artwork by Prozzta" with a
link to https://github.com/Prozzta/guppyos.

See src/renderer/src/assets/ATTRIBUTION.md for details.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,9 +556,15 @@ source, forks compile with no key and send nothing) are documented in
> LimeZu is required by that licence** and must stay in place. The Office cast is not LimeZu art. It
> is drawn procedurally in `portraitArt.ts`. See
> [`src/renderer/src/assets/ATTRIBUTION.md`](./src/renderer/src/assets/ATTRIBUTION.md).
>
> The **Star Hike** floor's art and map (`themes/starhike`) are **Artwork by Prozzta**
> (<https://github.com/Prozzta/guppyos>), under the pack's own licence in
> [`themes/starhike/LICENSE-ARTWORK.md`](./themes/starhike/LICENSE-ARTWORK.md). It permits editing and
> use in any project, not reselling or passing on the art by itself, and **the credit is required**.

The **source code** is licensed under the **MIT License**. See [`LICENSE`](./LICENSE). The MIT grant
covers the code only; the bundled pixel art is licensed separately from LimeZu and is carved out in
covers the code only; the bundled pixel art is licensed separately (from LimeZu, and from Prozzta for
Star Hike) and is carved out in
[`LICENSE-ASSETS`](./LICENSE-ASSETS). *Munder Difflin* is an affectionate parody and is not affiliated with NBC's *The Office* or
Dunder Mifflin.

Expand Down
104 changes: 104 additions & 0 deletions src/main/askMeAnswers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* The main side of `hive:answerQuestion` and `hive:answerQuestions`: an answer
* or a dismissal changes ONE humanQA entry, the one the card showed, on the
* latest tasks.json. The window never writes a card's whole humanQA list: its
* copy is up to 5 s old, and writing it back would undo an entry the god added
* since. Several requests (Dismiss all) are applied in order and written once.
*
* Pure and Electron free: Hive.answerQuestions reads and writes the ledger.
*/
import {
answerMail,
composeAnswer,
normalizeHumanQA,
normalizeOptionsIndexed,
openQuestionByRef,
type AnswerRequest,
type QuestionRef
} from '../shared/humanQuestion';

const MAX_ID = 200;

function isObject(v: unknown): v is Record<string, unknown> {
return !!v && typeof v === 'object' && !Array.isArray(v);
}

function parseRef(raw: unknown): QuestionRef | null {
if (!isObject(raw) || typeof raw.q !== 'string') return null;
if (!Number.isSafeInteger(raw.nth) || (raw.nth as number) < 0) return null;
if (raw.askedAt !== undefined && typeof raw.askedAt !== 'string') return null;
if (raw.labels !== undefined && (!Array.isArray(raw.labels) || !raw.labels.every((l) => typeof l === 'string'))) return null;
return {
q: raw.q,
nth: raw.nth as number,
...(typeof raw.askedAt === 'string' ? { askedAt: raw.askedAt } : {}),
...(Array.isArray(raw.labels) ? { labels: raw.labels as string[] } : {})
};
}

/** A well formed request, or null (reported as invalid). */
export function parseAnswerRequest(raw: unknown): AnswerRequest | null {
if (!isObject(raw)) return null;
const taskId = raw.taskId;
if (typeof taskId !== 'string' || !taskId || taskId.length > MAX_ID) return null;
const ref = parseRef(raw.ref);
if (!ref) return null;
if (raw.dismiss === true) return raw.answer === undefined ? { taskId, ref, dismiss: true } : null;
const a = raw.answer;
if (!isObject(a) || typeof a.text !== 'string') return null;
if (a.chosen !== undefined && (!Array.isArray(a.chosen) || !a.chosen.every((i) => Number.isSafeInteger(i) && (i as number) >= 0))) return null;
return { taskId, ref, answer: { text: a.text, ...(Array.isArray(a.chosen) ? { chosen: a.chosen as number[] } : {}) } };
}

export interface AppliedAnswer {
result: 'ok' | 'gone' | 'invalid';
/** For an answer that was applied: the god's inbox message. */
notice?: { subject: string; body: string };
}

/**
* Apply each request, in order, to the raw ledger list. Returns the next list
* (only changed cards replaced, every other field kept as written), whether
* anything changed, and one result per request.
*/
export function applyAnswers(
rawTasks: unknown[],
reqs: Array<AnswerRequest | null>,
nowIso: string
): { tasks: unknown[]; changed: boolean; outcomes: AppliedAnswer[] } {
const tasks = rawTasks.slice();
let changed = false;
const outcomes = reqs.map((req): AppliedAnswer => {
if (!req) return { result: 'invalid' };
const at = tasks.findIndex((t) => isObject(t) && t.id === req.taskId);
if (at < 0) return { result: 'gone' };
const card = tasks[at] as Record<string, unknown>;
const i = openQuestionByRef(card, req.ref, 'answer' in req);
if (i < 0) return { result: 'gone' };
const qa = (card.humanQA as unknown[]).slice();
const entry = qa[i] as Record<string, unknown>;
let notice: AppliedAnswer['notice'];
if ('dismiss' in req) {
qa[i] = { ...entry, dismissedAt: nowIso };
} else {
const shown = normalizeHumanQA(entry)!;
const options = shown.options ?? [];
const chosen = [...new Set(req.answer.chosen ?? [])].sort((a, b) => a - b);
if (chosen.some((c) => c >= options.length)) return { result: 'invalid' };
if (chosen.length > 1 && !shown.multi) return { result: 'invalid' };
const text = composeAnswer(options, chosen, req.answer.text);
if (!text) return { result: 'invalid' };
// The card's picks index the valid list; the ledger keeps raw positions.
const rawIndex = normalizeOptionsIndexed(entry.options)?.rawIndex ?? [];
const rest = { ...entry };
delete rest.chosen;
qa[i] = { ...rest, a: text, answeredAt: nowIso, ...(chosen.length ? { chosen: chosen.map((c) => rawIndex[c]) } : {}) };
const title = typeof card.title === 'string' ? card.title : req.taskId;
notice = answerMail({ id: req.taskId, title }, entry, { text, chosen });
}
tasks[at] = { ...card, humanQA: qa };
changed = true;
return notice ? { result: 'ok', notice } : { result: 'ok' };
});
return { tasks, changed, outcomes };
}
97 changes: 97 additions & 0 deletions src/main/codexAgentConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* What a hive Codex agent's OWN config.toml changes relative to the seed copied
* from the person's ~/.codex/config.toml (installCodexHooks). Pure text
* transforms; the person's file is only ever read, never written.
*
* Line based on purpose: a full TOML round trip would reformat the person's
* file and drop their comments.
*
* Ported from Prozzta/guppyos 02d0d50b (MIT).
*/

/** Each tool output the model sees is capped at this many of Codex's estimated
* tokens (about 4 characters each): an output over it keeps its beginning and
* end, and the middle is cut. The model's own policy still applies on top. */
export const CODEX_TOOL_OUTPUT_TOKEN_LIMIT = 4000;

/** A `[plugins.<name>]` table header (not an array of tables, not a sub-table).
* A bare TOML key is only [A-Za-z0-9_-]: a dot starts a dotted key (a sub-table
* such as `[plugins.browser.settings]`), and a name with '@' must be quoted. */
const PLUGIN_TABLE = /^\s*\[\s*plugins\s*\.\s*("(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*\]\s*(#.*)?$/;
const ANY_TABLE = /^\s*\[/;
const ENABLED = /^(\s*)(["']?)enabled\2(\s*=\s*)(true|false)(\s*(#.*)?)$/;

/**
* Every plugin the seed enables is turned OFF for a hive agent: each enabled
* plugin adds its tools, skills and instructions to every request (browser,
* computer use, documents, spreadsheets, ...), none of which a hive coding
* agent uses. A plugin table with no `enabled` key gets `enabled = false` too.
* Returns the text and how many were turned off.
*/
export function disableCodexPlugins(config: string): { text: string; disabled: number } {
const lines = config.split(/\r?\n/);
const out: string[] = [];
let inPlugin = false;
let sawEnabled = false;
let disabled = 0;
const closeTable = (): void => {
if (inPlugin && !sawEnabled) {
// After the table's last non-blank line, so a blank separator stays a separator.
let at = out.length;
while (at > 0 && out[at - 1].trim() === '') at--;
out.splice(at, 0, 'enabled = false # munder-hive: plugins are off for hive agents');
disabled++;
}
inPlugin = false;
sawEnabled = false;
};
for (const line of lines) {
if (ANY_TABLE.test(line)) {
closeTable();
inPlugin = PLUGIN_TABLE.test(line);
out.push(line);
continue;
}
const m = inPlugin ? ENABLED.exec(line) : null;
if (m) {
sawEnabled = true;
if (m[4] === 'true') {
out.push(`${m[1]}${m[2]}enabled${m[2]}${m[3]}false # munder-hive: plugins are off for hive agents`);
disabled++;
continue;
}
}
out.push(line);
}
closeTable();
return { text: out.join('\n'), disabled };
}

/** A top-level key line of the seed (before its first table), bare or quoted. */
function topLevelKey(line: string, key: string): boolean {
const esc = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`^\\s*(["']?)${esc}\\1\\s*=`).test(line);
}

/**
* Set top-level scalar keys in OUR copy: any seed line for the same key before
* the first table is removed (TOML forbids a duplicate key), and ours go first
* in the file. A `null` value only removes.
*/
export function setCodexTopLevelKeys(config: string, entries: Record<string, number | boolean | null>): string {
const lines = config.split(/\r?\n/);
const firstTable = lines.findIndex((l) => ANY_TABLE.test(l));
const topEnd = firstTable < 0 ? lines.length : firstTable;
const keys = Object.keys(entries);
const kept = lines.filter((l, i) => i >= topEnd || !keys.some((k) => topLevelKey(l, k)));
const ours = keys
.filter((k) => (typeof entries[k] === 'number' && Number.isFinite(entries[k] as number)) || typeof entries[k] === 'boolean')
.map((k) => `${k} = ${typeof entries[k] === 'boolean' ? String(entries[k]) : Math.trunc(entries[k] as number)}`);
if (!ours.length) return kept.join('\n');
return `# --- munder-hive: per-agent limits (auto-generated; do not edit) ---\n${ours.join('\n')}\n\n${kept.join('\n')}`;
}

/** OUR copy of the seed: plugins off, and each tool output capped. */
export function codexAgentConfig(seed: string): string {
return setCodexTopLevelKeys(disableCodexPlugins(seed).text, { tool_output_token_limit: CODEX_TOOL_OUTPUT_TOKEN_LIMIT });
}
Loading
Loading