Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
182 changes: 182 additions & 0 deletions extensions/vanamkarthiknetha/superdocs-writer-sidebar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# SuperDocs Writer Sidebar

A native LibreOffice Writer extension: a sidebar panel for sending the current
selection (or the whole document) to SuperDocs with a natural-language editing
instruction, reviewing the proposed changes, and applying approved ones back
into the document **range by range** — untouched text is never rewritten, and
existing paragraph/character styles are respected.

> **Status: v0.3.1 — selection, whole-document, and drafting modes, validated
> on Linux and Windows.** Selection mode: select a passage, describe the edit,
> review, Apply → only the selected range changes, styles intact.
> Whole-document mode (tick the checkbox): the doc is exported as .docx through
> the host's own filter and uploaded; every approved change is located by its
> original text in the live document and replaced range by range — a change
> that can't be found, or that matches more than one place, is **skipped and
> reported, never guessed at**. Drafting: when the instruction makes SuperDocs
> *create* content instead of editing ("create a gym plan"), the API applies it
> server-side with **zero pending changes** — creation bypasses the
> `ask_every_time` approval gate entirely (reported as a bug during this
> project). The sidebar closes that gap locally: the draft becomes a reviewable
> proposal, Apply inserts it at the cursor, Reject discards it — nothing lands
> in your document without a decision. Current limits, stated honestly:
> write-back and draft insertion are plain text into styled ranges (formatting
> *inside* a proposal isn't carried yet); whole-doc write-back covers body
> paragraphs, not table-cell content (those changes are reported as skipped,
> and the approved SuperDocs copy still has them).

**v0.3.3 — the panel is responsive.** It used to be a fixed-coordinate
layout that claimed a fixed height, so in a small office window the bottom of
the stack — the status line, the one control that says what just happened —
was placed outside the panel and clipped. The panel now reports its real
minimum/preferred/maximum height to the sidebar deck and re-lays itself out on
every resize: the flexible boxes (instruction, preview, status) share the
slack in a tall panel and give it back in a short one, the hint and title drop
out before anything functional does, and **Apply/Reject and the status line
stay on screen at every size**. An empty preview now takes no room at all, and
both the status and preview carry their full text as a tooltip. Sizes scale
with the host's DPI/font scaling rather than a hardcoded factor.

## Screenshots

**Selection mode (Linux, WSLg):**

| Sending a selection | Applied in place |
|---|---|
| ![Sending](screenshots/linux-sending.png) | ![Applied](screenshots/linux-applied.png) |

A `Heading 1` paragraph edited via the sidebar: the text changed, the heading
style survived untouched.

**Whole-document + drafting modes (Windows):**

| Draft requested | Draft gated, then inserted at the cursor |
|---|---|
| ![Requesting](screenshots/windows-draft-requesting.png) | ![Inserted](screenshots/windows-draft-inserted.png) |

"create a fitness plan" on a near-empty document: SuperDocs *creates* content
(zero pending changes server-side — the approval-gate bypass described above),
the sidebar gates it locally, and Apply inserts 27 paragraphs at the cursor —
the pre-existing sentence at the top is untouched, and the status line says
exactly what happened.

![Proposals](screenshots/windows-wholedoc-proposals.png)

A follow-up whole-document *edit* on that same draft ("Add some styles to it
like Markdown."): 22 proposed changes come back as a reviewable proposal with
the actual proposed text quoted — nothing lands until Apply.

**Small office window (Windows):**

![Small window](screenshots/windows-small-window.png)

The same panel in a 900×520 office window. The hint paragraph has dropped out
and the instruction box has shrunk to fit, but the checkbox, Send, Apply/Reject
and the status line ("Ready.") are all still on screen — before v0.3.3 the
status line was positioned below the panel's visible area and simply vanished.
Squeeze further and the title goes too; the working controls and the status
line are the last things standing. Below even that floor layout the sidebar
deck gives you its own scrollbar and the status is still reachable — the old
fixed height claimed the panel was 360px tall while its content was ~500px, so
the deck saw no reason to scroll and the overflow was simply lost.

## Build

```bash
python build.py # -> dist/superdocs-writer-sidebar.oxt
```

Stdlib-only build; the source layout under `src/` is the .oxt layout.

## Install (Linux)

```bash
sudo apt install libreoffice-writer python3-uno # python3-uno = Python component loader
unopkg add -f dist/superdocs-writer-sidebar.oxt
```

Then open Writer → **View → Sidebar** → the **SuperDocs** deck appears in the
tab rail. (Windows note for development: use `unopkg.com`, not `unopkg.exe` —
the latter detaches from the console and can fail silently.)

## How it talks to SuperDocs

- The selection goes up as passage text (`/v1/documents/upload-base64` into a
fresh session) — SuperDocs takes documents, never raw XML. In whole-document
mode the doc is exported via the host's own filter (`storeToURL`, MS Word
2007 XML) and uploaded as a file, per the host-app integration contract.
- `POST /v1/chat/async` with `approval_mode: "ask_every_time"`; the panel polls
the job and previews the **actual proposed content** (`new_html`, tag-stripped)
rather than the AI's self-description, which can paraphrase.
- Approvals always send the explicit `changes` array — a bare
`{job_id, approved:true}` is a silent no-op in the API (discovered and
reported during this project).
- Selection write-back: the approved result (plain-text export of the session)
goes through the stored UNO text range with `setString`, inheriting the
range's current styles. Whole-document write-back (`writeback.py`): each
approved change's `old_html` is reduced to plain-text paragraphs, located as
one consecutive paragraph run in the live document, and replaced with a
style-preserving `setString` — no hard formatting, no whole-file replacement,
untouched text is never rewritten. Not-found and ambiguous matches are
skipped and named in the status line.
- All network work runs on a background thread; UI and document updates are
marshaled to the office main thread via `AsyncCallback`.
- API key: `SUPERDOCS_API_KEY` environment variable, else
`~/.superdocs/agent_credentials.json`. Never hardcoded, never in this repo.

## Tests

All harnesses (Linux) start a throwaway headless LibreOffice on an isolated
profile; they never touch your normal LibreOffice or its profile.

- `tests/run_style_test.sh` — builds a document with a Heading 1, a bullet
list, and a table, edits one body paragraph through the exact client +
write-back path the panel uses, and asserts the heading/list/table styles and
content all survived. Costs 1 operation. Ends with `STYLE TEST: ALL PASS`.
- `tests/run_whole_doc_test.sh` — **offline, no API key, 0 operations.**
Drives `writeback.apply_changes` with synthetic changes against a real Writer
doc and proves the write-back invariants: unique match applied in place
(entities decoded, inline tags stripped), a multi-paragraph run replaced as
one range, ambiguous and not-found changes skipped with both copies
untouched, table content untouched. Ends with
`WHOLE-DOC WRITE-BACK TEST: ALL PASS`.
- `tests/run_whole_doc_live_test.sh` — the same flow end to end against the
real API: UNO doc → .docx export → `request_doc_edit` → approve → write-back
from the actual returned `old_html`/`new_html`. Costs 1 operation. Ends with
`WHOLE-DOC LIVE TEST: ALL PASS`.
- `tests/layout_test.py` — **offline, plain `python3`, no LibreOffice, no API
key, 0 operations.** Drives `panel_layout.solve` at every panel height from
below the floor to well above the preferred layout, at three DPI scales, and
asserts the invariants the panel promises: the status line is always fully
inside the panel and always below the decision buttons, the working controls
never disappear, rows never overlap or overflow the width, spare height is
absorbed instead of left dangling. ~38k assertions over ~2.2k panel sizes;
ends with `PANEL LAYOUT TEST: ALL PASS`.
- `tests/draft_live_test.py` — plain HTTP, no LibreOffice needed, ≤1 operation.
Sends a drafting instruction and verifies the client surfaces the created
content as `draft_html` on a completed zero-pending job (the approval-gate
bypass). If the model happens to propose in-place edits instead, they are
rejected (free) and the run reports SKIPPED honestly — model choice between
editing and creating is nondeterministic.

(Portability note discovered by this test: LibreOffice 26 dropped the classic
"List Bullet" paragraph-style family — bullets are now "List 1".."List 5" — so
the test discovers style names at runtime instead of assuming them.)

## Files

| Path | Role |
|------|------|
| `src/description.xml` | Extension identity/version |
| `src/META-INF/manifest.xml` | Declares the two .xcu configs + Python component |
| `src/config/Sidebar.xcu` | Registers the SuperDocs deck + panel in Writer's sidebar |
| `src/config/Factories.xcu` | Maps the panel's resource URL to the Python factory |
| `src/panel.py` | UNO component: panel factory, panel, threading, both modes |
| `src/panel.xdl` | Panel UI (whole-doc checkbox, instruction box, Send, preview, Apply/Reject, status) — authoring geometry; runtime positions come from `panel_layout` |
| `src/pythonpath/panel_layout.py` | Responsive geometry: where every control goes for the panel's current size (pure arithmetic, no UNO) |
| `src/pythonpath/superdocs_client.py` | Stdlib SuperDocs client (upload → async chat → approve → export) |
| `src/pythonpath/writeback.py` | Whole-doc write-back: HTML→paragraphs, unique-run matching, range-by-range apply, draft insertion |
| `src/icons/superdocs_24.png` | Sidebar deck icon |

Built by Karthik Vanam for the SuperDocs hiring round, with AI-assisted
development (Claude Code).
22 changes: 22 additions & 0 deletions extensions/vanamkarthiknetha/superdocs-writer-sidebar/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""Build the .oxt: zip everything under src/ into dist/superdocs-writer-sidebar.oxt."""
import zipfile
from pathlib import Path

ROOT = Path(__file__).parent
SRC = ROOT / "src"
DIST = ROOT / "dist"


def main():
DIST.mkdir(exist_ok=True)
oxt = DIST / "superdocs-writer-sidebar.oxt"
with zipfile.ZipFile(oxt, "w", zipfile.ZIP_DEFLATED) as z:
for path in sorted(SRC.rglob("*")):
if path.is_file() and "__pycache__" not in path.parts:
z.write(path, path.relative_to(SRC).as_posix())
print(f"built {oxt} ({oxt.stat().st_size} bytes)")


if __name__ == "__main__":
main()
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest xmlns:manifest="http://openoffice.org/2001/manifest">
<manifest:file-entry manifest:media-type="application/vnd.sun.star.configuration-data"
manifest:full-path="config/Sidebar.xcu"/>
<manifest:file-entry manifest:media-type="application/vnd.sun.star.configuration-data"
manifest:full-path="config/Factories.xcu"/>
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python"
manifest:full-path="panel.py"/>
</manifest:manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
oor:name="Factories" oor:package="org.openoffice.Office.UI">
<node oor:name="Registered">
<node oor:name="UIElementFactories">
<node oor:name="SuperDocsPanelFactory" oor:op="replace">
<prop oor:name="Type" oor:type="xs:string">
<value>toolpanel</value>
</prop>
<prop oor:name="Name" oor:type="xs:string">
<value>SuperDocsPanelFactory</value>
</prop>
<prop oor:name="Module" oor:type="xs:string">
<value/>
</prop>
<prop oor:name="FactoryImplementation" oor:type="xs:string">
<value>app.superdocs.writer.PanelFactory</value>
</prop>
</node>
</node>
</node>
</oor:component-data>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
oor:name="Sidebar" oor:package="org.openoffice.Office.UI">
<node oor:name="Content">
<node oor:name="DeckList">
<node oor:name="SuperDocsDeck" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value>SuperDocs</value>
</prop>
<prop oor:name="Id" oor:type="xs:string">
<value>SuperDocsDeck</value>
</prop>
<prop oor:name="IconURL" oor:type="xs:string">
<value>vnd.sun.star.extension://app.superdocs.writer.sidebar.vanamkarthiknetha/icons/superdocs_24.png</value>
</prop>
<prop oor:name="ContextList">
<value oor:separator=";">
WriterVariants, any, visible ;
</value>
</prop>
<prop oor:name="OrderIndex" oor:type="xs:int">
<value>500</value>
</prop>
</node>
</node>
<node oor:name="PanelList">
<node oor:name="SuperDocsPanel" oor:op="replace">
<prop oor:name="Title" oor:type="xs:string">
<value>AI editing</value>
</prop>
<prop oor:name="Id" oor:type="xs:string">
<value>SuperDocsPanel</value>
</prop>
<prop oor:name="DeckId" oor:type="xs:string">
<value>SuperDocsDeck</value>
</prop>
<prop oor:name="ContextList">
<value oor:separator=";">
WriterVariants, any, visible ;
</value>
</prop>
<prop oor:name="ImplementationURL" oor:type="xs:string">
<value>private:resource/toolpanel/SuperDocsPanelFactory/SuperDocsPanel</value>
</prop>
<prop oor:name="OrderIndex" oor:type="xs:int">
<value>100</value>
</prop>
</node>
</node>
</node>
</oor:component-data>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<description xmlns="http://openoffice.org/extensions/description/2006"
xmlns:xlink="http://www.w3.org/1999/xlink">
<identifier value="app.superdocs.writer.sidebar.vanamkarthiknetha"/>
<version value="0.3.3"/>
<platform value="all"/>
<display-name>
<name lang="en">SuperDocs Writer Sidebar</name>
</display-name>
</description>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading